硅谷课堂-智慧星球 Day 11~Day 12——尚硅谷项目笔记 2022 年

硅谷课堂-智慧星球 Day 11~Day 12——尚硅谷项目笔记 2022 年

文章目录

Day 11-营销管理模块和公众号菜单管理

一、优惠券列表接口

1、编写获取用户信息接口
1.1、创建 service-user 模块

(1)获取优惠券详情时候,需要获取使用者的昵称和手机号,所以使用远程调用实现此功能。

创建 service-user 模块

创建 service-user 模块

1.2、生成相关代码

生成相关代码

1.3、创建启动类
package com.myxh.smart.planet.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author MYXH
 * @date 2023/10/15
 */
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.myxh.smart.planet.user.mapper")
public class ServiceUserApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ServiceUserApplication.class, args);
    }
}
1.4、创建配置文件
# 服务端口
server.port=8304

# 服务名
spring.application.name=service-user

# 环境设置:dev、test、prod
spring.profiles.active=dev

# MySQL 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/smart_planet_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=MYXH
spring.datasource.password=520.ILY!

# 返回 Json 的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

# MyBatis 日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 设置 mapper.xml 的路径
mybatis-plus.mapper-locations=classpath:com/myxh/smart/planet/user/mapper/xml/*.xml

# nacos 服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
1.5、编写 UserInfocontroller

实现根据用户 id 获取用户信息接口。

package com.myxh.smart.planet.user.controller;

import com.myxh.smart.planet.model.user.UserInfo;
import com.myxh.smart.planet.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/15
 *
 * <p>
 * 用户信息 前端控制器
 * </p>
 */
@Tag(name = "用户信息管理", description = "用户信息管理接口")
@RestController
@RequestMapping("/admin/user/user/info")
public class UserInfoController
{
    @Autowired
    private UserInfoService userService;

    /**
     * 根据 id 查询用户信息
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "查询", description = "根据 id 查询用户信息")
    @GetMapping("inner/get/{id}")
    public UserInfo getUserInfoById(@PathVariable("id") Long id)
    {
        UserInfo userInfo = userService.getById(id);

        return userInfo;
    }
}
1.6、配置网关

在网关配置文件配置路径。

# service-user 模块配置
# 设置路由 id
spring.cloud.gateway.routes[3].id=service-user
# 设置路由的 uri,lb 全称为 Load Balance 负载平衡
spring.cloud.gateway.routes[3].uri=lb://service-user
# 设置路由断言,代理 servicerId 为 auth-service 的 /auth/ 路径,/admin/user/user/info
spring.cloud.gateway.routes[3].predicates= Path=/*/user/**
2、创建模块定义远程接口
2.1、创建模块

在 SmartPlanet/service-client/service-user-client。

创建模块

2.2、service-client 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.myxh.smart.planet</groupId>
        <artifactId>SmartPlanet</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>service-client</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>service-user-client</module>
    </modules>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- model 数据载体 -->
        <dependency>
            <groupId>com.myxh.smart.planet</groupId>
            <artifactId>model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>

        <!-- service-utils -->
        <dependency>
            <groupId>com.myxh.smart.planet</groupId>
            <artifactId>service-utils</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>

        <!-- Web 需要启动项目 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- openfeign 服务调用 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
2.3、定义远程调用的接口
package com.myxh.smart.planet.client.user;

import com.myxh.smart.planet.model.user.UserInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author MYXH
 * @date 2023/10/15
 */
@FeignClient("service-user")
public interface UserInfoFeignClient
{
    @Operation(summary = "查询", description = "根据 id 查询用户信息")
    @GetMapping("/admin/user/user/info/inner/get/{id}")
    UserInfo getUserInfoById(@PathVariable("id") Long id);
}
3、编写 Service 实现方法
3.1、service 引入依赖
<!-- 负载平衡 loadbalancer -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
3.2、service-activity 引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.myxh.smart.planet</groupId>
        <artifactId>service</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>service-activity</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- service-user-client -->
        <dependency>
            <groupId>com.myxh.smart.planet</groupId>
            <artifactId>service-user-client</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <!-- mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.32</version>
        </dependency>
    </dependencies>
</project>
3.2、service-activity 添加注解
package com.myxh.smart.planet.activity;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;



/**
 * @author MYXH
 * @date 2023/10/15
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.myxh.smart.planet")
@MapperScan("com.myxh.smart.planet.activity.mapper")
public class ServiceActivityApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ServiceActivityApplication.class, args);
    }
}
3.3、CouponInfoServiceImpl 实现方法

远程调用,根据用户 id 获取用户信息。

package com.myxh.smart.planet.activity.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.activity.mapper.CouponInfoMapper;
import com.myxh.smart.planet.activity.service.CouponInfoService;
import com.myxh.smart.planet.activity.service.CouponUseService;
import com.myxh.smart.planet.client.user.UserInfoFeignClient;
import com.myxh.smart.planet.model.activity.CouponInfo;
import com.myxh.smart.planet.model.activity.CouponUse;
import com.myxh.smart.planet.model.user.UserInfo;
import com.myxh.smart.planet.vo.activity.CouponUseQueryVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/15
 *
 * <p>
 * 优惠券信息 服务实现类
 * </p>
 */
@Service
public class CouponInfoServiceImpl extends ServiceImpl<CouponInfoMapper, CouponInfo> implements CouponInfoService
{
    @Autowired
    private CouponUseService couponUseService;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    /**
     * 优惠券信息列表
     *
     * @param couponUsePageParam 优惠券使用页面参数
     * @param couponUseQueryVo   查询对象
     * @return couponUsePageModel 优惠券使用页面
     */
    @Override
    public IPage<CouponUse> selectCouponUsePage(Page<CouponUse> couponUsePageParam, CouponUseQueryVo couponUseQueryVo)
    {
        // 获取条件
        Long couponId = couponUseQueryVo.getCouponId();
        String couponStatus = couponUseQueryVo.getCouponStatus();
        String getTimeBegin = couponUseQueryVo.getGetTimeBegin();
        String getTimeEnd = couponUseQueryVo.getGetTimeEnd();

        // 封装条件
        QueryWrapper<CouponUse> wrapper = new QueryWrapper<>();

        if (!ObjectUtils.isEmpty(couponId))
        {
            wrapper.eq("coupon_id", couponId);
        }
        if (StringUtils.hasLength(couponStatus))
        {
            wrapper.eq("coupon_status", couponStatus);
        }
        if (StringUtils.hasLength(getTimeBegin))
        {
            wrapper.ge("get_time", getTimeBegin);
        }
        if (StringUtils.hasLength(getTimeEnd))
        {
            wrapper.le("get_time", getTimeEnd);
        }

        // 调用方法查询
        IPage<CouponUse> CouponUsePage = couponUseService.page(couponUsePageParam, wrapper);

        // 封装用户昵称和手机号
        List<CouponUse> couponUseList = CouponUsePage.getRecords();

        // 遍历
        couponUseList.stream().forEach(this::getUserInfoByCouponUse);

        return CouponUsePage;
    }

    /**
     * 封装用户昵称和手机号
     *
     * @param couponUse 优惠券使用数据
     * @return couponUse 优惠券使用数据
     */
    private CouponUse getUserInfoByCouponUse(CouponUse couponUse)
    {
        // 获取用户 id
        Long userId = couponUse.getUserId();

        if (!ObjectUtils.isEmpty(userId))
        {
            // 远程调用
            UserInfo userInfo = userInfoFeignClient.getUserInfoById(userId);

            if (userInfo != null)
            {
                couponUse.getParam().put("nickName", userInfo.getNickName());
                couponUse.getParam().put("phone", userInfo.getPhone());
            }
        }

        return couponUse;
    }
}
4、配置网关
4.1、配置路由规则

(1)service-gateway 配置文件。

# service-activity 模块配置
# 设置路由 id
spring.cloud.gateway.routes[2].id=service-activity
# 设置路由的 uri,lb 全称为 Load Balance 负载平衡
spring.cloud.gateway.routes[2].uri=lb://service-activity
# 设置路由断言,代理 servicerId 为 auth-service 的 /auth/ 路径,/admin/activity/coupon/info
spring.cloud.gateway.routes[2].predicates= Path=/*/activity/**
5、整合优惠券前端
5.1、定义接口

(1)创建 api/activity/couponInfo.js。

定义接口

import request from "@/utils/request";

const COUPON_INFO_API = "/admin/activity/coupon/info";

export default {
  /**
   * 优惠券信息列表
   *
   * @param {number} current 当前页码
   * @param {number} limit 每页记录数
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getPageList(current, limit) {
    return request({
      url: `${COUPON_INFO_API}/find/query/page/${current}/${limit}`,
      method: "get",
    });
  },

  /**
   * 根据 id 获取优惠券信息
   *
   * @param {number} id id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getById(id) {
    return request({
      url: `${COUPON_INFO_API}/get/${id}`,
      method: "get",
    });
  },

  /**
   * 添加优惠券信息
   *
   * @param {Object} couponInfo 优惠券信息
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  save(couponInfo) {
    return request({
      url: `${COUPON_INFO_API}/save`,
      method: "post",
      data: couponInfo,
    });
  },

  /**
   * 根据 id 修改优惠券信息
   *
   * @param {Object} couponInfo 优惠券信息
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateById(couponInfo) {
    return request({
      url: `${COUPON_INFO_API}/update`,
      method: "put",
      data: couponInfo,
    });
  },

  /**
   * 删除优惠券信息
   *
   * @param {number} id id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeById(id) {
    return request({
      url: `${COUPON_INFO_API}/remove/${id}`,
      method: "delete",
    });
  },

  /**
   * 批量删除优惠券信息
   *
   * @param {Array}idList id 数组,Json 数组 [1,2,3,...]
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeRows(idList) {
    return request({
      url: `${COUPON_INFO_API}/remove/batch`,
      method: "delete",
      data: idList,
    });
  },

  /**
   * 已经使用的优惠券信息列表
   *
   * @param {number} current 当前页码
   * @param {number} limit 每页记录数
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getPageCouponUseList(current, limit, searchObj) {
    return request({
      url: `${COUPON_INFO_API}/coupon/use/${current}/${limit}`,
      method: "get",
      params: searchObj,
    });
  },
};
5.2、创建路由

(1)router/index.js 定义路由。

  // 营销活动管理
  {
    path: "/activity",
    component: Layout,
    redirect: "/coupon/info/list",
    name: "Activity",
    meta: { title: "营销活动管理", icon: "el-icon-football" },
    alwaysShow: true,
    children: [
      {
        path: "coupon/info/list",
        name: "CouponInfo",
        component: () => import("@/views/activity/couponInfo/list"),
        meta: { title: "优惠券列表" },
      },
      {
        path: "coupon/info/add",
        name: "CouponInfoAdd",
        component: () => import("@/views/activity/couponInfo/form"),
        meta: { title: "添加优惠券" },
      },
      {
        path: "coupon/info/edit/:id",
        name: "CouponInfoEdit",
        component: () => import("@/views/activity/couponInfo/form"),
        meta: { title: "编辑优惠券", noCache: true },
        hidden: true,
      },
      {
        path: "coupon/info/show/:id",
        name: "CouponInfoShow",
        component: () => import("@/views/activity/couponInfo/show"),
        meta: { title: "优惠券详情", noCache: true },
        hidden: true,
      },
    ],
  },
5.3、创建 vue 页面

(1)创建 views/activity/couponInfo/页面。

创建 vue 页面

(2)list.vue

<template>
  <div class="app-container">
    <!-- 工具条 -->
    <el-card class="operate-container" shadow="never">
      <i class="el-icon-tickets" style="margin-top: 5px"></i>
      <span style="margin-top: 5px">数据列表</span>
      <el-button class="btn-add" size="mini" @click="add()">添加</el-button>
    </el-card>

    <!-- banner 列表 -->
    <el-table
      v-loading="listLoading"
      :data="list"
      element-loading-text="数据正在加载......"
      border
      fit
      highlight-current-row
    >
      <el-table-column label="序号" width="70" align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>

      <el-table-column prop="couponName" label="购物券名称" />
      <el-table-column prop="couponType" label="购物券类型">
        <template slot-scope="scope">
          {{ scope.row.couponType === "1" ? "注册卷" : "推荐赠送卷" }}
        </template>
      </el-table-column>
      <el-table-column label="规则">
        <template slot-scope="scope">
          {{ "现金卷:" + scope.row.amount + "元" }}
        </template>
      </el-table-column>
      <el-table-column label="使用范围 "> 所有商品 </el-table-column>
      <el-table-column prop="publishCount" label="发行数量" />
      <el-table-column prop="expireTime" label="过期时间" />
      <el-table-column prop="createTime" label="创建时间" />
      <el-table-column label="操作" width="150" align="center">
        <template slot-scope="scope">
          <router-link :to="'/activity/coupon/info/edit/' + scope.row.id">
            <el-button size="mini" type="text">修改</el-button>
          </router-link>
          <el-button
            size="mini"
            type="text"
            @click="removeDataById(scope.row.id)"
            >删除</el-button
          >
          <router-link :to="'/activity/coupon/info/show/' + scope.row.id">
            <el-button size="mini" type="text">详情</el-button>
          </router-link>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
      :current-page="page"
      :total="total"
      :page-size="limit"
      :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
      style="padding: 30px 0; text-align: center"
      layout="sizes, prev, pager, next, jumper, ->, total, slot"
      @current-change="fetchData"
      @size-change="changeSize"
    />
  </div>
</template>

<script>
  import couponInfoAPI from "@/api/activity/couponInfo";

  export default {
    data() {
      return {
        // 数据是否正在加载
        listLoading: true,
        // banner列表
        list: null,
        // 数据库中的总记录数
        total: 0,
        // 默认页码
        page: 1,
        // 每页记录数
        limit: 10,
        // 查询表单对象
        searchObj: {},
        // 批量选择中选择的记录列表
        multipleSelection: [],
      };
    },

    // 生命周期函数:内存准备完毕,页面尚未渲染
    created() {
      console.log("list created...");
      this.fetchData();
    },

    // 生命周期函数:内存准备完毕,页面渲染成功
    mounted() {
      console.log("list mounted...");
    },

    methods: {
      // 当页码发生改变的时候
      changeSize(size) {
        console.log(size);
        this.limit = size;
        this.fetchData(1);
      },

      add() {
        this.$router.push({ path: "/activity/coupon/info/add" });
      },

      // 加载 banner 列表数据
      fetchData(page = 1) {
        console.log("翻页" + page);
        // 异步获取远程数据(ajax)
        this.page = page;

        couponInfoAPI
          .getPageList(this.page, this.limit, this.searchObj)
          .then((response) => {
            this.list = response.data.records;
            this.total = response.data.total;

            // 数据加载并绑定成功
            this.listLoading = false;
          });
      },

      // 重置查询表单
      resetData() {
        console.log("重置查询表单");
        this.searchObj = {};
        this.fetchData();
      },

      // 根据 id 删除数据
      removeDataById(id) {
        // debugger
        this.$confirm("此操作将永久删除该记录, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            // promise
            // 点击确定,远程调用ajax
            return couponInfoAPI.removeById(id);
          })
          .then((response) => {
            this.fetchData(this.page);
            if (response.code) {
              this.$message({
                type: "success",
                message: "删除成功!",
              });
            }
          })
          .catch(() => {
            this.$message({
              type: "info",
              message: "已取消删除",
            });
          });
      },
    },
  };
</script>

(3)form.vue

<template>
  <div class="app-container">
    <el-form label-width="120px">
      <el-form-item label="优惠券名称">
        <el-input v-model="couponInfo.couponName" />
      </el-form-item>
      <el-form-item label="优惠券类型">
        <el-radio-group v-model="couponInfo.couponType">
          <el-radio label="1">注册卷</el-radio>
          <el-radio label="2">推荐购买卷</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="金额">
        <el-input-number v-model="couponInfo.amount" :min="0" />
      </el-form-item>
      <el-form-item label="发行数量">
        <el-input-number v-model="couponInfo.publishCount" :min="0" />
      </el-form-item>
      <el-form-item label="领取时间">
        <el-date-picker
          v-model="couponInfo.startTime"
          type="date"
          placeholder="选择开始日期"
          value-format="yyyy-MM-dd"
        /><el-date-picker
          v-model="couponInfo.endTime"
          type="date"
          placeholder="选择结束日期"
          value-format="yyyy-MM-dd"
        />
      </el-form-item>
      <el-form-item label="过期时间">
        <el-date-picker
          v-model="couponInfo.expireTime"
          type="datetime"
          placeholder="选择过期时间"
          value-format="yyyy-MM-dd HH:mm:ss"
        />
      </el-form-item>
      <el-form-item label="直播详情">
        <el-input v-model="couponInfo.ruleDesc" type="textarea" rows="5" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="saveOrUpdate">保存</el-button>
        <el-button @click="back">返回</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
  import couponInfoAPI from "@/api/activity/couponInfo";

  const defaultForm = {
    id: "",
    couponType: "1",
    couponName: "",
    amount: "0",
    conditionAmount: "0",
    startTime: "",
    endTime: "",
    rangeType: "1",
    ruleDesc: "",
    publishCount: "",
    perLimit: "1",
    useCount: "0",
    receiveCount: "",
    expireTime: "",
    publishStatus: "",
  };

  export default {
    data() {
      return {
        couponInfo: defaultForm,
        saveBtnDisabled: false,

        keyword: "",
        skuInfoList: [],
      };
    },

    // 监听器
    watch: {
      $route(to, from) {
        console.log("路由变化...");
        console.log(to);
        console.log(from);
        this.init();
      },
    },

    // 生命周期方法(在路由切换,组件不变的情况下不会被调用)
    created() {
      console.log("form created...");
      this.init();
    },

    methods: {
      // 表单初始化
      init() {
        // debugger
        if (this.$route.params && this.$route.params.id) {
          const id = this.$route.params.id;
          this.fetchDataById(id);
        } else {
          // 对象拓展运算符:拷贝对象,而不是赋值对象的引用
          this.couponInfo = { ...defaultForm };
        }
      },

      saveOrUpdate() {
        // 防止表单重复提交
        this.saveBtnDisabled = true;

        if (!this.couponInfo.couponName) {
          this.$message.error("请输入优惠券名称");
          this.saveBtnDisabled = false;

          return;
        }

        if (!this.couponInfo.publishCount) {
          this.$message.error("请输入发行数量");
          this.saveBtnDisabled = false;

          return;
        }

        // 验证开始时间和结束时间的合法性
        if (!this.validateDateRange()) {
          return;
        }

        if (!this.couponInfo.id) {
          this.saveData();
        } else {
          this.updateData();
        }
      },

      // 验证开始时间和结束时间的合法性
      validateDateRange() {
        if (
          (this.couponInfo.startTime &&
            this.couponInfo.endTime &&
            this.couponInfo.startTime > this.couponInfo.endTime) ||
          this.couponInfo.endTime > this.couponInfo.expireTime
        ) {
          this.$message.error("开始时间不能晚于结束时间");
          return false;
        }
        return true;
      },

      // 新增
      saveData() {
        couponInfoAPI.save(this.couponInfo).then((response) => {
          if (response.code) {
            this.$message({
              type: "success",
              message: response.message,
            });
            this.$router.push({ path: "/activity/coupon/info/list" });
          }
        });
      },

      // 根据 id 更新记录
      updateData() {
        couponInfoAPI.updateById(this.couponInfo).then((response) => {
          debugger;
          if (response.code) {
            this.$message({
              type: "success",
              message: response.message,
            });
            this.$router.push({ path: "/activity/coupon/info/list" });
          }
        });
      },

      back() {
        this.$router.push({ path: "/activity/coupon/info/list" });
      },

      // 根据 id 查询记录
      fetchDataById(id) {
        couponInfoAPI.getById(id).then((response) => {
          this.couponInfo = response.data;
        });
      },
    },
  };
</script>

(4)show.vue

<template>
  <div class="app-container">
    <h4>优惠券信息</h4>
    <table
      class="table table-striped table-condenseda table-bordered"
      width="100%"
    >
      <tbody>
        <tr>
          <th width="15%">优惠券名称</th>
          <td width="35%">
            <b style="font-size: 14px">{{ couponInfo.couponName }}</b>
          </td>
          <th width="15%">优惠券类型</th>
          <td width="35%">
            {{ couponInfo.couponType === "1" ? "注册卷" : "推荐赠送卷" }}
          </td>
        </tr>
        <tr>
          <th>发行数量</th>
          <td>{{ couponInfo.publishCount }}</td>
          <th>每人限领次数</th>
          <td>{{ couponInfo.perLimit }}</td>
        </tr>
        <tr>
          <th>领取数量</th>
          <td>{{ couponInfo.receiveCount }}</td>
          <th>使用数量</th>
          <td>{{ couponInfo.useCount }}</td>
        </tr>
        <tr>
          <th>领取时间</th>
          <td>{{ couponInfo.startTime }}至{{ couponInfo.endTime }}</td>
          <th>过期时间</th>
          <td>{{ couponInfo.expireTime }}</td>
        </tr>
        <tr>
          <th>规则描述</th>
          <td colspan="3">{{ couponInfo.ruleDesc }}</td>
        </tr>
      </tbody>
    </table>

    <h4>优惠券发放列表&nbsp;&nbsp;&nbsp;</h4>
    <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%; margin-top: 10px"
    >
      <el-table-column label="序号" width="70" align="center">
        <template slot-scope="scope">
          {{ (page - 1) * limit + scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column prop="param.nickName" label="用户昵称" />
      <el-table-column prop="param.phone" label="手机号" />
      <el-table-column label="使用状态">
        <template slot-scope="scope">
          {{ scope.row.couponStatus == "NOT_USED" ? "未使用" : "已使用" }}
        </template>
      </el-table-column>
      <el-table-column prop="getTime" label="获取时间" />
      <el-table-column prop="usingTime" label="使用时间" />
      <el-table-column prop="usedTime" label="支付时间" />
      <el-table-column prop="expireTime" label="过期时间" />
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
      :current-page="page"
      :total="total"
      :page-size="limit"
      :page-sizes="[5, 10, 20, 30, 40, 50, 100]"
      style="padding: 30px 0; text-align: center"
      layout="sizes, prev, pager, next, jumper, ->, total, slot"
      @current-change="fetchData"
      @size-change="changeSize"
    />

    <div style="margin-top: 15px">
      <el-form label-width="0px">
        <el-form-item>
          <el-button @click="back">返回</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>
  import couponInfoAPI from "@/api/activity/couponInfo";

  export default {
    data() {
      return {
        // 数据是否正在加载
        listLoading: false,
        couponId: null,
        couponInfo: {},
        // banner列表
        list: null,
        // 数据库中的总记录数
        total: 0,
        // 默认页码
        page: 1,
        // 每页记录数
        limit: 10,
        // 查询表单对象
        searchObj: {},
      };
    },

    // 监听器
    watch: {
      $route(to, from) {
        console.log("路由变化...");
        console.log(to);
        console.log(from);
        this.init();
      },
    },

    // 生命周期方法(在路由切换,组件不变的情况下不会被调用)
    created() {
      console.log("form created...");
      this.couponId = this.$route.params.id;
      // 获取优惠券信息
      this.fetchDataById();
      this.fetchData();
    },

    methods: {
      // 根据 id 查询记录
      fetchDataById() {
        couponInfoAPI.getById(this.couponId).then((response) => {
          this.couponInfo = response.data;
        });
      },

      // 当页码发生改变的时候
      changeSize(size) {
        console.log(size);
        this.limit = size;
        this.fetchData(1);
      },

      // 加载 banner 列表数据
      fetchData(page = 1) {
        console.log("翻页..." + page);
        // 异步获取远程数据(ajax)
        this.page = page;
        this.searchObj.couponId = this.couponId;
        couponInfoAPI
          .getPageCouponUseList(this.page, this.limit, this.searchObj)
          .then((response) => {
            this.list = response.data.records;
            this.total = response.data.total;

            // 数据加载并绑定成功
            this.listLoading = false;
          });
      },

      back() {
        this.$router.push({ path: "/activity/coupon/info/list" });
      },
    },
  };
</script>

二、微信公众号

1、注册公众号

微信公众平台:https://mp.weixin.qq.com/

注册公众号

硅谷课堂要求基于 H5,具有微信支付等高级功能的,因此需要注册服务号,订阅号不具备支付功能。

注册步骤参考官方注册文档:https://kf.qq.com/faq/120911VrYVrA151013MfYvYV.html

注册过程仅做了解,有公司运营负责申请与认证。

2、公众号功能介绍

在微信公众平台扫码登录后可以发现管理页面左侧菜单栏有丰富的功能:

公众号功能介绍

大概可以分为这几大模块:

首页内容与互动数据广告与服务设置与开发新功能

作为开发人员,首先应该关注的是设置与开发模块;而作为产品运营人员与数据分析人员,关注的是内容与互动、数据及广告与服务模块。

首先不妨各个功能模块都点击看一看,大概了解下能做些什么。可以确认的是,这个微信公众平台当然不只是给开发人员使用的,它提供了很多非技术人员可在UI 界面上交互操作的功能模块。

如配置消息回复、自定义菜单、发布文章等:

公众号功能介绍

这个时候可能会想:这些功能好像非技术人员都能随意操作,那么还需要技术人员去开发吗?

答案是: 如果只是日常简单的推送文章,就像关注的大多数公众号一样,那确实不需要技术人员去开发;但是,如果你想将你们的网站嵌入进去公众号菜单里(这里指的是把前端项目的首页链接配置在自定义菜单),并且实现微信端的独立登录认证、获取微信用户信息、微信支付等高级功能,或者觉得 UI 交互的配置方式无法满足你的需求,你需要更加自由、随心所欲的操作,那么就必须启用开发者模式了,通过技术人员的手段去灵活控制公众号。

这里有一点需要注意,如果决定技术人员开发公众号,必须启用服务器配置,而这将导致 UI 界面设置的自动回复和自定义菜单失效!

设置与开发 - 基本配置 - 服务器配置 中点击启用:

公众号功能介绍

公众号功能介绍

至于服务器配置中的选项代表什么意思、如何填写,下面再讲。

3、微信公众平台测试帐号
3.1、申请测试帐号

微信公众平台接口测试帐号:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login&token=399029368&lang=zh_CN

申请测试帐号

申请测试帐号

3.2、查看测试号管理

(1)其中 appID 和 appsecret 用于后面菜单开发使用。

(2)其中 URL 是开发者用来接收微信消息和事件的接口 URL。Token 可由开发者可以任意填写,用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)。本地测试,url 改为内网穿透地址。

查看测试号管理

3.3、关注公众号

关注公众号

关注公众号

4、开发业务介绍

硅谷课堂涉及的微信公众号功能模块:自定义菜单、消息、微信支付、授权登录等。

三、后台管理系统-公众号菜单管理

1、需求分析
1.1、微信自定义菜单说明

微信自定义菜单文档地址:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html

微信自定义菜单注意事项:

  1. 自定义菜单最多包括 3 个一级菜单,每个一级菜单最多包含 5 个二级菜单。

  2. 一级菜单最多 4 个汉字,二级菜单最多 8 个汉字,多出来的部分将会以“…”代替。

  3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号 profile 页时,如果发现上一次拉取菜单的请求在 5 分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

1.2、硅谷课堂自定义菜单

一级菜单:直播、课程、我的。

二级菜单:根据一级菜单动态设置二级菜单,直播(近期直播课程),课程(课程分类),我的(我的订单、我的课程、我的优惠券以及关于我们)。

说明:

​ 1、二级菜单可以是网页类型,点击跳转 H5 页面。

​ 2、二级菜单可以是消息类型,点击返回消息。

1.3、数据格式

自定义菜单通过后台管理设置到数据库表,数据配置好后,通过微信接口推送菜单数据到微信平台。

表结构(menu):

数据格式

数据格式

表示例数据:

数据格式

1.4、管理页面

(1)页面功能“列表、添加、修改与删除”是对 menu 表的操作。

(2)页面功能“同步菜单与删除菜单”是对微信平台接口操作。

管理页面

2、搭建菜单管理后端环境
2.1、创建模块 service-wechat

(1)在 service 下创建子模块 service-wechat。

创建模块 service-wechat

(2)引入依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.myxh.smart.planet</groupId>
        <artifactId>service</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>service-wechat</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.32</version>
        </dependency>

        <!-- weixin-java-mp -->
        <dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-mp</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>
</project>
2.2、生成菜单相关代码

生成菜单相关代码

2.3、创建启动类和配置文件

(1)启动类。

package com.myxh.smart.planet.wechat;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author MYXH
 * @date 2023/10/16
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.myxh.smart.planet")
@MapperScan("com.myxh.smart.planet.wechat.mapper")
public class ServiceWechatApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ServiceWechatApplication.class, args);
    }
}

(2)配置文件。

# 服务端口
server.port=8305

# 服务名
spring.application.name=service-wechat

# 环境设置:dev、test、prod
spring.profiles.active=dev

# MySQL 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/smart_planet_wechat?characterEncoding=utf-8&useSSL=false
spring.datasource.username=MYXH
spring.datasource.password=520.ILY!

# 返回 Json 的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

# MyBatis 日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 设置 mapper.xml 的路径
mybatis-plus.mapper-locations=classpath:com/myxh/smart/planet/wechat/mapper/xml/*.xml

# nacos 服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# 公众号 id 和秘钥
# 智慧星球微信公众平台 appId
wechat.appId=wxc23b80b9ffaac7bd
# 智慧星球微信公众平台 api 秘钥
wechat.appSecret=5c0271622c4271753310c436b5cd3532
2.4、配置网关
# service-wechat 模块配置
# 设置路由 id
spring.cloud.gateway.routes[4].id=service-wechat
# 设置路由的 uri,lb 全称为 Load Balance 负载平衡
spring.cloud.gateway.routes[4].uri=lb://service-wechat
# 设置路由断言,代理 servicerId 为 auth-service 的 /auth/ 路径,/admin/wechat/menu
spring.cloud.gateway.routes[4].predicates= Path=/*/wechat/**
3、开发菜单管理接口
3.1、编写 MenuController
package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.model.wechat.Menu;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.wechat.MenuVo;
import com.myxh.smart.planet.wechat.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 前端控制器
 * </p>
 */
@Tag(name = "微信公众号菜单管理", description = "微信公众号菜单管理接口")
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController
{
    @Autowired
    private MenuService menuService;

    /**
     * 获取所有菜单,按照一级和二级菜单封装
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取所有菜单", description = "获取所有菜单,按照一级和二级菜单封装")
    @GetMapping("find/menu/info")
    public Result<List<MenuVo>> findMenuInfo()
    {
        List<MenuVo> menuList = menuService.findMenuInfo();

        return Result.ok(menuList);
    }

    /**
     * 获取所有一级菜单
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取所有一级菜单", description = "获取所有一级菜单")
    @GetMapping("find/one/menu/info")
    public Result<List<Menu>> findOneMenuInfo()
    {
        List<Menu> oneMenuList = menuService.findMenuOneInfo();

        return Result.ok(oneMenuList);
    }

    /**
     * 根据 id 查询菜单
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "查询", description = "根据 id 查询菜单")
    @GetMapping("get/{id}")
    public Result<Menu> get(@PathVariable("id") Long id)
    {
        Menu menu = menuService.getById(id);

        return Result.ok(menu);
    }

    /**
     * 添加菜单
     *
     * @param menu 菜单
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "添加", description = "添加菜单")
    @PostMapping("save")
    public Result<Void> save(@RequestBody Menu menu)
    {
        menuService.save(menu);

        return Result.ok(null);
    }

    /**
     * 修改菜单
     *
     * @param menu 菜单
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "修改", description = "修改菜单")
    @PutMapping("update")
    public Result<Void> updateById(@RequestBody Menu menu)
    {
        menuService.updateById(menu);

        return Result.ok(null);
    }

    /**
     * 逻辑删除菜单
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除", description = "逻辑删除菜单")
    @DeleteMapping("remove/{id}")
    public Result<Void> remove(@PathVariable("id") Long id)
    {
        menuService.removeById(id);

        return Result.ok(null);
    }

    /**
     * 批量删除菜单
     *
     * @param idList id 数组,Json 数组 [1,2,3,...]
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "批量删除", description = "批量删除菜单")
    @DeleteMapping("remove/batch")
    public Result<Void> batchRemove(@RequestBody List<Long> idList)
    {
        menuService.removeByIds(idList);

        return Result.ok(null);
    }
}
3.2、编写 Service

(1)MenuService 定义方法。

package com.myxh.smart.planet.wechat.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.wechat.Menu;
import com.myxh.smart.planet.vo.wechat.MenuVo;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务类
 * </p>
 */
public interface MenuService extends IService<Menu>
{
    /**
     * 获取所有菜单,按照一级和二级菜单封装
     *
     * @return menuList 菜单列表
     */
    List<MenuVo> findMenuInfo();

    /**
     * 获取所有一级菜单
     *
     * @return oneMenuList 一级菜单列表
     */
    List<Menu> findMenuOneInfo();
}

(2)MenuServiceImpl 实现方法。

package com.myxh.smart.planet.wechat.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.wechat.Menu;
import com.myxh.smart.planet.vo.wechat.MenuVo;
import com.myxh.smart.planet.wechat.mapper.MenuMapper;
import com.myxh.smart.planet.wechat.service.MenuService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

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

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务实现类
 * </p>
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService
{
    /**
     * 获取所有菜单,按照一级和二级菜单封装
     *
     * @return menuList 菜单列表
     */
    @Override
    public List<MenuVo> findMenuInfo()
    {
        // 1、创建 List 集合,用于最终数据封装
        List<MenuVo> finalMenuList = new ArrayList<>();

        // 2、查询所有菜单数据(包含一级和二级)
        List<Menu> menuList = baseMapper.selectList(null);

        // 3、从所有菜单数据获取所有一级菜单数据(parentId = 0)
        List<Menu> oneMenuList = menuList.stream().filter(menu -> menu.getParentId() == 0).toList();

        // 4、封装一级菜单数据,封装到最终数据 List 集台
        // 遍历一级菜单 List 集合
        for (Menu oneMenu : oneMenuList)
        {
            MenuVo oneMenuVo = new MenuVo();
            BeanUtils.copyProperties(oneMenu, oneMenuVo);

            // 5、封装二级菜单数据(判断一级菜单和二级菜单 parentId是否相同)
            // 如果相同,把二级菜单数据放到一级菜单里面
            List<Menu> twoMenuList = menuList.stream().filter(menu -> menu.getParentId().longValue() == oneMenu.getId()).sorted(Comparator.comparing(Menu::getSort)).toList();

            List<MenuVo> children = new ArrayList<>();

            for (Menu twoMenu : twoMenuList)
            {
                MenuVo twoMenuVo = new MenuVo();
                BeanUtils.copyProperties(twoMenu, twoMenuVo);
                children.add(twoMenuVo);
            }

            // 把二级菜单数据放到一级菜单里面
            oneMenuVo.setChildren(children);

            // 把 oneMenuVo 放到最终 List 集合
            finalMenuList.add(oneMenuVo);
        }

        // 返回最终数据
        return finalMenuList;
    }

    /**
     * 获取所有一级菜单
     *
     * @return oneMenuList 一级菜单列表
     */
    @Override
    public List<Menu> findMenuOneInfo()
    {
        QueryWrapper<Menu> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_id", 0);
        List<Menu> oneMenuList = baseMapper.selectList(wrapper);

        return oneMenuList;
    }
}
4、同步菜单(获取 access_token)
4.1、文档查看

(1)进行菜单同步时候,需要获取到公众号的 access_token,通过 access_token 进行菜单同步。

接口文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

文档查看

(2)调用方式。

文档查看

文档查看

4.2、service-wechat 添加配置
# 公众号 id 和秘钥
# 智慧星球微信公众平台 appId
wechat.appId=wxc23b80b9ffaac7bd
# 智慧星球微信公众平台 api 秘钥
wechat.appSecret=5c0271622c4271753310c436b5cd3532
4.3、添加工具类
package com.myxh.smart.planet.wechat.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/17
 * @description 常量类,读取配置文件 application.properties 中的配置
 */
@Component
public class ConstantPropertiesUtil implements InitializingBean
{
    @Value("${wechat.appId}")
    private String appId;

    @Value("${wechat.appSecret}")
    private String appSecret;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;

    @Override
    public void afterPropertiesSet() throws Exception
    {
        ACCESS_KEY_ID = appId;
        ACCESS_KEY_SECRET = appSecret;
    }
}
4.4、复制 HttpClient 工具类

复制 HttpClient 工具类

4.5、添加 Menucontroller 方法
package com.myxh.smart.planet.wechat.controller;

import com.alibaba.fastjson2.JSONObject;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.wechat.utils.ConstantPropertiesUtil;
import com.myxh.smart.planet.wechat.utils.HttpClientUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 前端控制器
 * </p>
 */
@Tag(name = "微信公众号菜单管理", description = "微信公众号菜单管理接口")
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController
{
    /**
     * 获取 access_token
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取 access_token", description = "获取 access_token")
    @GetMapping("get/access/token")
    public Result<String> getAccessToken()
    {
        try
        {
            // 拼接请求地址
            String buffer = "https://api.weixin.qq.com/cgi-bin/token" +
                    "?grant_type=client_credential" +
                    "&appid=%s" +
                    "&secret=%s";

            // 请求地址设置参数
            String url = String.format(buffer,
                    ConstantPropertiesUtil.ACCESS_KEY_ID,
                    ConstantPropertiesUtil.ACCESS_KEY_SECRET);

            // 发送 http 请求
            String tokenString = HttpClientUtils.get(url);

            // 获取 access_token
            JSONObject jsonObject = JSONObject.parseObject(tokenString);
            String access_token = jsonObject.getString("access_token");

            // 返回
            return Result.ok(access_token);
        }
        catch (Exception e)
        {
            e.printStackTrace();

            throw new SmartPlanetException(20001, "获取 access_token 失败");
        }
    }
}
4.6、测试
5、同步菜单(功能实现)

接口文档:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html

接口调用请求说明:

http 请求方式:POST(请使用 https 协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

weixin-java-mp是封装好了的微信接口客户端,使用起来很方便,后续就使用 weixin-java-mp 处理微信平台接口。

5.1、添加配置类
package com.myxh.smart.planet.wechat.config;

import com.myxh.smart.planet.wechat.utils.ConstantPropertiesUtil;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@Component
public class WeChatMpConfig
{

    @Autowired
    public WeChatMpConfig(ConstantPropertiesUtil constantPropertiesUtil)
    {

    }

    @Bean
    public WxMpService wxMpService(WxMpConfigStorage wxMpConfigStorage)
    {
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage);

        return wxMpService;
    }

    @Bean
    public WxMpConfigStorage wxMpConfigStorage()
    {
        WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
        wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
        wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);

        return wxMpConfigStorage;
    }
}
5.2、定义 Service 方法

MenuService

package com.myxh.smart.planet.wechat.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.wechat.Menu;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务类
 * </p>
 */
public interface MenuService extends IService<Menu>
{
    /**
     * 同步微信公众号菜单
     */
    void syncMenu();
}
5.3、实现 Service 方法

MenuServiceImpl

package com.myxh.smart.planet.wechat.service.impl;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.wechat.Menu;
import com.myxh.smart.planet.vo.wechat.MenuVo;
import com.myxh.smart.planet.wechat.mapper.MenuMapper;
import com.myxh.smart.planet.wechat.service.MenuService;
import lombok.SneakyThrows;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务实现类
 * </p>
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService
{
    @Autowired
    private WxMpService wxMpService;

    /**
     * 同步微信公众号菜单
     */
    @SneakyThrows
    @Override
    public void syncMenu()
    {
        // 获取所有菜单数据
        List<MenuVo> menuVoList = this.findMenuInfo();

        // 封装 button 里面结构,数组格式
        JSONArray buttonList = new JSONArray();

        for (MenuVo oneMenuVo : menuVoList)
        {
            // json 对象,一级菜单
            JSONObject one = new JSONObject();
            one.put("name", oneMenuVo.getName());

            // json 数组,二级菜单
            JSONArray subButton = new JSONArray();

            for (MenuVo twoMenuVo : oneMenuVo.getChildren())
            {
                JSONObject view = new JSONObject();
                view.put("type", twoMenuVo.getType());
                view.put("name", twoMenuVo.getName());

                if (twoMenuVo.getType().equals("view"))
                {
                    view.put("url", "http://smartplanetmobile.free.idcfengye.com/#" + twoMenuVo.getUrl());
                }
                else
                {
                    view.put("key", twoMenuVo.getMenuKey());
                }

                subButton.add(view);
            }

            one.put("sub_button", subButton);
            buttonList.add(one);
        }

        // 封装最外层 button 部分
        JSONObject button = new JSONObject();
        button.put("button", buttonList);

        try
        {
            String menuId = this.wxMpService.getMenuService().menuCreate(button.toJSONString());
            System.out.println("menuId = " + menuId);

        }
        catch (Exception e)
        {
            throw new SmartPlanetException(20001, "微信公众号菜单同步失败");
        }
    }
}
5.4、controller 方法
package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.wechat.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 前端控制器
 * </p>
 */
@Tag(name = "微信公众号菜单管理", description = "微信公众号菜单管理接口")
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController
{
    @Autowired
    private MenuService menuService;

    /**
     * 同步微信公众号菜单
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "同步微信公众号菜单", description = "同步微信公众号菜单")
    @GetMapping("sync/menu")
    public Result<Void> createMenu()
    {
        menuService.syncMenu();

        return Result.ok(null);
    }
}
6、删除菜单
6.1、service 接口
package com.myxh.smart.planet.wechat.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.wechat.Menu;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务类
 * </p>
 */
public interface MenuService extends IService<Menu>
{
    /**
     * 删除微信公众号菜单
     */
    void removeMenu();
}
6.2、service 接口实现
package com.myxh.smart.planet.wechat.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.wechat.Menu;
import com.myxh.smart.planet.wechat.mapper.MenuMapper;
import com.myxh.smart.planet.wechat.service.MenuService;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 服务实现类
 * </p>
 */
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService
{
    @Autowired
    private WxMpService wxMpService;

    /**
     * 删除微信公众号菜单
     */
    @Override
    public void removeMenu()
    {
        try
        {
            wxMpService.getMenuService().menuDelete();
        }
        catch (WxErrorException e)
        {
            throw new SmartPlanetException(20001, "微信公众号菜单删除失败");
        }
    }
}
6.3、controller 方法
package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.wechat.service.MenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/16
 *
 * <p>
 * 菜单 前端控制器
 * </p>
 */
@Tag(name = "微信公众号菜单管理", description = "微信公众号菜单管理接口")
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController
{
    @Autowired
    private MenuService menuService;

    /**
     * 删除微信公众号菜单
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除微信公众号菜单", description = "删除微信公众号菜单")
    @DeleteMapping("remove/menu")
    public Result<Void> removeMenu()
    {
        menuService.removeMenu();

        return Result.ok(null);
    }
}
7、开发菜单管理前端
7.1、添加路由

(1)src/router/index.js 添加路由。

// 微信公众号菜单管理
{
  path: "/wechat",
  component: Layout,
  redirect: "/wechat/menu/list",
  name: "Wechat",
  meta: {
    title: "微信公众号菜单管理",
    icon: "el-icon-refrigerator",
  },
  alwaysShow: true,
  children: [
    {
      path: "menu/list",
      name: "Menu",
      component: () => import("@/views/wechat/menu/list"),
      meta: { title: "菜单列表" },
    },
  ],
},
7.2、定义接口

(1)src/api/wechat/menu.js 定义接口。

import request from "@/utils/request";

const MENU_API = "/admin/wechat/menu";

export default {
  /**
   * 获取所有菜单,按照一级和二级菜单封装
   *
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  findMenuInfo() {
    return request({
      url: `${MENU_API}/find/menu/info`,
      method: `get`,
    });
  },

  /**
   * 获取所有一级菜单
   *
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  findOneMenuInfo() {
    return request({
      url: `${MENU_API}/find/one/menu/info`,
      method: `get`,
    });
  },

  /**
   * 根据 id 查询菜单
   *
   * @param {number} id id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getById(id) {
    return request({
      url: `${MENU_API}/get/${id}`,
      method: `get`,
    });
  },

  /**
   * 添加菜单
   *
   * @param {Object} menu 菜单
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  save(menu) {
    return request({
      url: `${MENU_API}/save`,
      method: `post`,
      data: menu,
    });
  },

  /**
   * 修改菜单
   *
   * @param {Object} menu 菜单
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  updateById(menu) {
    return request({
      url: `${MENU_API}/update`,
      method: `put`,
      data: menu,
    });
  },

  /**
   * 逻辑删除菜单
   *
   * @param {number} id id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeById(id) {
    return request({
      url: `${MENU_API}/remove/${id}`,
      method: "delete",
    });
  },

  /**
   * 同步微信公众号菜单
   *
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  syncMenu() {
    return request({
      url: `${MENU_API}/sync/menu`,
      method: `get`,
    });
  },

  /**
   * 删除微信公众号菜单
   *
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  removeMenu() {
    return request({
      url: `${MENU_API}/remove/menu`,
      method: `delete`,
    });
  },
};
7.3、编写页面

(1)创建 views/wechat/menu/list.vue。

<template>
  <div class="app-container">
    <!-- 工具条 -->
    <el-card class="operate-container" shadow="never">
      <i class="el-icon-tickets" style="margin-top: 5px"></i>
      <span style="margin-top: 5px">数据列表</span>
      <el-button
        class="btn-add"
        size="mini"
        @click="removeMenu"
        style="margin-left: 10px"
        >删除菜单</el-button
      >
      <el-button class="btn-add" size="mini" @click="syncMenu"
        >同步菜单</el-button
      >
      <el-button class="btn-add" size="mini" @click="add">添 加</el-button>
    </el-card>

    <el-table
      :data="list"
      style="width: 100%; margin-bottom: 20px"
      row-key="id"
      border
      default-expand-all
      :tree-props="{ children: 'children' }"
    >
      <el-table-column label="名称" prop="name" width="350"></el-table-column>
      <el-table-column label="类型" width="100">
        <template slot-scope="scope">
          {{ scope.row.type === "view" ? "链接" : scope.row.type == "click" ?
          "事件" : "" }}
        </template>
      </el-table-column>
      <el-table-column label="菜单URL" prop="url"></el-table-column>
      <el-table-column
        label="菜单KEY"
        prop="menuKey"
        width="130"
      ></el-table-column>
      <el-table-column label="排序号" prop="sort" width="70"></el-table-column>
      <el-table-column label="操作" width="170" align="center">
        <template slot-scope="scope">
          <el-button
            v-if="scope.row.parentId > 0"
            type="text"
            size="mini"
            @click="edit(scope.row.id)"
            >修改</el-button
          >
          <el-button
            v-if="scope.row.parentId > 0"
            type="text"
            size="mini"
            @click="removeDataById(scope.row.id)"
            >删除</el-button
          >
        </template>
      </el-table-column>
    </el-table>

    <el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%">
      <el-form
        ref="flashPromotionForm"
        label-width="150px"
        size="small"
        style="padding-right: 40px"
      >
        <el-form-item label="选择一级菜单">
          <el-select v-model="menu.parentId" placeholder="请选择">
            <el-option
              v-for="item in list"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item v-if="menu.parentId == 1" label="菜单名称">
          <el-select
            v-model="menu.name"
            placeholder="请选择"
            @change="liveCourseChanged"
          >
            <el-option
              v-for="item in liveCourseList"
              :key="item.id"
              :label="item.courseName"
              :value="item"
            />
          </el-select>
        </el-form-item>
        <el-form-item v-if="menu.parentId == 2" label="菜单名称">
          <el-select
            v-model="menu.name"
            placeholder="请选择"
            @change="subjectChanged"
          >
            <el-option
              v-for="item in subjectList"
              :key="item.id"
              :label="item.title"
              :value="item"
            />
          </el-select>
        </el-form-item>
        <el-form-item v-if="menu.parentId == 3" label="菜单名称">
          <el-input v-model="menu.name" />
        </el-form-item>
        <el-form-item label="菜单类型">
          <el-radio-group v-model="menu.type">
            <el-radio label="view">链接</el-radio>
            <el-radio label="click">事件</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item v-if="menu.type == 'view'" label="链接">
          <el-input v-model="menu.url" />
        </el-form-item>
        <el-form-item v-if="menu.type == 'click'" label="菜单KEY">
          <el-input v-model="menu.menuKey" />
        </el-form-item>
        <el-form-item label="排序">
          <el-input v-model="menu.sort" />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false" size="small">取 消</el-button>
        <el-button type="primary" @click="saveOrUpdate()" size="small"
          >确 定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>

<script>
  import menuAPI from "@/api/wechat/menu";
  // import liveCourseAPI from '@/api/live/liveCourse'
  import subjectAPI from "@/api/vod/subject";

  const defaultForm = {
    id: null,
    parentId: 1,
    name: "",
    nameId: null,
    sort: 1,
    type: "view",
    menuKey: "",
    url: "",
  };

  export default {
    // 定义数据
    data() {
      return {
        list: [],
        liveCourseList: [],
        subjectList: [],
        dialogVisible: false,
        menu: defaultForm,
        saveBtnDisabled: false,
      };
    },

    // 当页面加载时获取数据
    created() {
      this.fetchData();
      // this.fetchLiveCourse()
      this.fetchSubject();
    },

    methods: {
      // 调用 api 层获取数据库中的数据
      fetchData() {
        console.log("加载列表");
        menuAPI.findMenuInfo().then((response) => {
          this.list = response.data;
          console.log(this.list);
        });
      },

      /*
    fetchLiveCourse() {
      liveCourseAPI.findLatelyList().then((response) => {
        this.liveCourseList = response.data;
        this.liveCourseList.push({ id: 0, courseName: "全部列表" });
      });
    },
     */

      fetchSubject() {
        console.log("加载列表");
        subjectAPI.getChildList(0).then((response) => {
          this.subjectList = response.data;
        });
      },

      syncMenu() {
        this.$confirm("你确定上传菜单吗, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            return menuAPI.syncMenu();
          })
          .then((response) => {
            this.fetchData();
            this.$message.success(response.message);
          })
          .catch((error) => {
            console.log("error", error);
            // 当取消时会进入 catch 语句:error = 'cancel'
            // 当后端服务抛出异常时:error = 'error'
            if (error === "cancel") {
              this.$message.info("取消上传");
            }
          });
      },

      removeMenu() {
        this.$confirm("你确定删除菜单吗, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            return menuAPI.removeMenu();
          })
          .then((response) => {
            this.fetchData();
            this.$message.success(response.message);
          })
          .catch((error) => {
            console.log("error", error);
            // 当取消时会进入 catch 语句:error = 'cancel'
            // 当后端服务抛出异常时:error = 'error'
            if (error === "cancel") {
              this.$message.info("取消删除");
            }
          });
      },

      // 根据 id 删除数据
      removeDataById(id) {
        this.$confirm("此操作将永久删除该记录, 是否继续?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        })
          .then(() => {
            // 点击确定,远程调用 ajax
            return menuAPI.removeById(id);
          })
          .then((response) => {
            this.fetchData(this.page);
            if (response.code) {
              this.$message({
                type: "success",
                message: "删除成功!",
              });
            }
          })
          .catch(() => {
            this.$message({
              type: "info",
              message: "已取消删除",
            });
          });
      },

      add() {
        this.dialogVisible = true;
        this.menu = Object.assign({}, defaultForm);
      },

      edit(id) {
        this.dialogVisible = true;
        this.fetchDataById(id);
      },

      fetchDataById(id) {
        menuAPI.getById(id).then((response) => {
          this.menu = response.data;
        });
      },

      saveOrUpdate() {
        // 防止表单重复提交
        this.saveBtnDisabled = true;

        if (!this.menu.name) {
          this.$message.error("请输入菜单名称");
          this.saveBtnDisabled = false;
          return;
        }

        if (!this.menu.id) {
          this.saveData();
        } else {
          this.updateData();
        }
      },

      // 新增
      saveData() {
        menuAPI.save(this.menu).then((response) => {
          if (response.code) {
            this.$message({
              type: "success",
              message: response.message,
            });
            this.dialogVisible = false;
            this.fetchData(this.page);
          }
        });
      },

      // 根据 id 更新记录
      updateData() {
        menuAPI.updateById(this.menu).then((response) => {
          if (response.code) {
            this.$message({
              type: "success",
              message: response.message,
            });
            this.dialogVisible = false;
            this.fetchData(this.page);
          }
        });
      },

      // 根据 id 查询记录
      fetchDataById(id) {
        menuAPI.getById(id).then((response) => {
          this.menu = response.data;
        });
      },

      subjectChanged(item) {
        console.info(item);
        this.menu.name = item.title;
        this.menu.url = "/course/" + item.id;
      },

      liveCourseChanged(item) {
        console.info(item);
        this.menu.name = item.courseName;
        if (item.id == 0) {
          this.menu.url = "/live";
        } else {
          this.menu.url = "/live/info/" + item.id;
        }
      },
    },
  };
</script>
8、公众号菜单功能测试

(1)在手机公众号可以看到同步之后的菜单。

公众号菜单功能测试

Day 12-公众号消息和微信授权登录

一、公众号普通消息

1、实现目标

1、“智慧星球”公众号实现根据关键字搜索相关课程,如:输入“Java”,可返回 Java 相关的一个课程。

2、“智慧星球”公众号点击菜单“关于我们”,返回关于我们的介绍。

3、关注或取消关注等。

2、消息接入

参考文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

接入微信公众平台开发,开发者需要按照如下步骤完成:

1、填写服务器配置。

2、验证服务器地址的有效性。

3、依据接口文档实现业务逻辑。

2.1、公众号服务器配置

在测试管理 -> 接口配置信息,点击“修改”按钮,填写服务器地址(URL)和 Token,其中 URL 是开发者用来接收微信消息和事件的接口 URL。Token 可由开发者可以任意填写,用作生成签名(该 Token 会和接口 URL 中包含的 Token 进行比对,从而验证安全性)。

说明:本地测试,url 改为内网穿透地址。

公众号服务器配置

2.2、验证来自微信服务器消息

(1)概述。

开发者提交信息后,微信服务器将发送 GET 请求到填写的服务器地址 URL 上,GET 请求携带参数如下表所示:

参数描述
signature微信加密签名,signature 结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce 参数。
timestamp时间戳。
nonce随机数。
echostr随机字符串。

开发者通过检验 signature 对请求进行校验(下面有校验方式)。若确认此次 GET 请求来自微信服务器,请原样返回 echostr 参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

1、将 token、timestamp、nonce 三个参数进行字典序排序。

2、将三个参数字符串拼接成一个字符串进行 sha1 加密。

3、开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信。

(2)代码实现。

创建 MessageController。

package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.wechat.utils.SHA1;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController
{
    private static final String token = "SmartPlanet";

    /**
     * 服务器有效性验证
     *
     * @param request 请求
     * @return echostr 随机字符串
     */
    @GetMapping
    public String verifyToken(HttpServletRequest request)
    {
        String signature = request.getParameter("signature");
        String timestamp = request.getParameter("timestamp");
        String nonce = request.getParameter("nonce");
        String echostr = request.getParameter("echostr");

        System.out.println("signature: " + signature + " nonce: " + nonce + " echostr: " + echostr + "timestamp: " + timestamp);

        if (this.checkSignature(signature, timestamp, nonce))
        {
            System.out.println("token ok");

            return echostr;
        }

        return echostr;
    }

    private boolean checkSignature(String signature, String timestamp, String nonce)
    {
        String[] str = new String[]{token, timestamp, nonce};

        // 排序
        Arrays.sort(str);

        // 拼接字符串
        StringBuilder buffer = new StringBuilder();

        for (String s : str)
        {
            buffer.append(s);
        }

        // 进行 SHA1 加密
        String temp = SHA1.encode(buffer.toString());

        // 与微信提供的 signature 进行匹对
        return signature.equals(temp);
    }
}

完成之后,校验接口就算是开发完成了。接下来就可以开发消息接收接口了。

2.3、消息接收

接下来开发消息接收接口,消息接收接口和上面的服务器校验接口地址是一样的,都是一开始在公众号后台配置的地址。只不过消息接收接口是一个 POST 请求。

在公众号后台配置的时候,消息加解密方式选择了明文模式,这样在后台收到的消息直接就可以处理了。微信服务器给我发来的普通文本消息格式如下:

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test]]></Content>
    <MsgId>1234567890123456</MsgId>
    <MsgDataId>xxxx</MsgDataId>
    <Idx>xxxx</Idx>
</xml>
参数描述
ToUserName开发者微信号。
FromUserName发送方帐号(一个 OpenID)。
CreateTime消息创建时间(整型)。
MsgType消息类型,文本为 text。
Content文本消息内容。
MsgId消息 id,64 位整型。
MsgDataId消息的数据 ID(消息如果来自文章时才有)。
Idx多图文时第几篇文章,从 1 开始(消息如果来自文章时才有)。

看到这里,心里大概就有数了,当收到微信服务器发来的消息之后,就进行 XML 解析,提取出来需要的信息,去做相关的查询操作,再将查到的结果返回给微信服务器。

这里先来个简单的,将收到的消息解析并打印出来:

package com.myxh.smart.planet.wechat.controller;

import com.alibaba.fastjson2.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author MYXH
 * @date 2023/10/17
 */
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController
{
    /**
     * 接收微信服务器发送来的消息
     *
     * @param request 请求
     * @return message 消息
     * @throws Exception 异常
     */
    @PostMapping
    public String receiveMessage(HttpServletRequest request) throws Exception
    {
        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
        System.out.println(JSONObject.toJSONString(wxMpXmlMessage));

        return "success";
    }

    private Map<String, String> parseXml(HttpServletRequest request) throws Exception
    {
        Map<String, String> map = new HashMap<>();
        InputStream inputStream = request.getInputStream();
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();
        List<Element> elementList = root.elements();

        for (Element e : elementList)
        {
            map.put(e.getName(), e.getText());
        }

        inputStream.close();
        inputStream = null;

        return map;
    }
}
3、配置内网穿透(ngrok)
3.1、注册用户

网址:https://ngrok.cc/login/register

注册用户

3.2、实名认证

(1)注册成功之后,登录系统,进行实名认证,认证费 2 元,认证通过后才能开通隧道。

实名认证

3.3、开通隧道

(1)选择隧道管理 -> 开通隧道。

最后一个是免费服务器,建议选择付费服务器,10 元/月,因为免费服务器使用人数很多,经常掉线。

开通隧道

(2)点击立即购买 -> 输入相关信息。

开通隧道

(3)开通成功后,查看开通的隧道。

这里开通了两个隧道,一个用于后端接口调用,一个用于公众号前端调用。

开通隧道

开通隧道

3.4、启动隧道

(1)下载客户端工具。

启动隧道

(2)选择 windows 版本。

启动隧道

(3)解压,找到 bat 文件,双击启动。

启动隧道

(4)输入隧道 id,多个使用逗号隔开,最后回车就可以启动。

启动隧道

启动隧道

3.5、测试

启动服务,在智慧星球公众号发送文本消息测试效果。

4、消息业务实现
4.1、service-vod 模块创建接口

(1)创建 CourseApiController 方法,根据课程关键字查询课程信息。

service-vod 模块创建接口

package com.myxh.smart.planet.vod.api;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@RestController
@RequestMapping("/api/vod/course")
public class CourseApiController
{
    @Autowired
    private CourseService courseService;

    /**
     * 根据关键字查询课程
     * @param keyword 关键字
     * @return courseList 课程信息
     */
    @Operation(summary = "根据关键字查询课程", description = "根据关键字查询课程")
    @GetMapping("inner/find/by/keyword/{keyword}")
    public List<Course> findByKeyword(
            @Parameter(name = "keyword",description = "关键字", required = true)
            @PathVariable("keyword") String keyword)
    {
        QueryWrapper<Course> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("title", keyword);
        List<Course> courseList = courseService.list(queryWrapper);

        return courseList;
    }
}
4.2、创建模块定义接口

(1)service-client 下创建子模块 service-course-client。

创建模块定义接口

(2)定义根据关键字查询课程接口。

package com.myxh.smart.planet.client.course;

import com.myxh.smart.planet.model.vod.Course;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@FeignClient("service-vod")
public interface CourseFeignClient
{
    @Operation(summary = "根据关键字查询课程", description = "根据关键字查询课程")
    @GetMapping("/api/vod/course/inner/find/by/keyword/{keyword}")
    List<Course> findByKeyword(@PathVariable("keyword") String keyword);
}
4.3、service-wechat 引入依赖
<!-- service-course-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-course-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
4.4、service-wechat 模块实现方法

(1)MessageService

package com.myxh.smart.planet.wechat.service;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/17
 */
public interface MessageService
{
    /**
     * 接收微信服务器发送来的消息
     *
     * @param param 请求参数
     * @return message 消息
     */
    String receiveMessage(Map<String, String> param);
}

(2)MessageServiceImpl

package com.myxh.smart.planet.wechat.service.impl;

import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.wechat.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@Service
public class MessageServiceImpl implements MessageService
{
    @Autowired
    private CourseFeignClient courseFeignClient;

    /**
     * 接收微信服务器发送来的消息
     *
     * @param param 请求参数
     * @return message 消息
     */
    @Override
    public String receiveMessage(Map<String, String> param)
    {
        String content;

        try
        {
            String msgType = param.get("MsgType");

            // 判断是什么类型消息
            switch (msgType)
            {
                // 普通文本类型,输入关键字 Java
                case "text":
                    content = this.search(param);
                    break;
                // 事件类型
                case "event":
                    String event = param.get("Event");
                    String eventKey = param.get("EventKey");

                    if ("subscribe".equals(event))
                    {
                        // 关注公众号
                        content = this.subscribe(param);
                    }
                    else if ("unsubscribe".equals(event))
                    {
                        // 取消关注公众号
                        content = this.unsubscribe(param);
                    }
                    else if ("CLICK".equals(event) && "aboutUs".equals(eventKey))
                    {
                        // 关于我们
                        content = this.aboutUs(param);
                    }
                    else
                    {
                        content = "success";
                    }
                    break;
                default:
                    // 其他情况
                    content = "success";
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
            content = this.text(param, "请重新输入关键字,没有匹配到相关视频课程").toString();
        }

        return content;
    }

    /**
     * 处理关键字搜索事件
     * 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复 1 条图文消息;其余场景最多可回复 8 条图文消息
     *
     * @param param 请求参数
     * @return message 消息
     */
    private String search(Map<String, String> param)
    {
        String fromUsername = param.get("FromUserName");
        String toUsername = param.get("ToUserName");
        String content = param.get("Content");

        // 单位为秒,不是毫秒
        long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();
        List<Course> courseList = courseFeignClient.findByKeyword(content);

        if (CollectionUtils.isEmpty(courseList))
        {
            text = this.text(param, "请重新输入关键字,没有匹配到相关视频课程");
        }
        else
        {
            // 一次只能返回一个
            Random random = new Random();
            int num = random.nextInt(courseList.size());
            Course course = courseList.get(num);

            String articles = "<item>" +
                    "<Title><![CDATA[" + course.getTitle() + "]]></Title>" +
                    "<Description><![CDATA[" + course.getTitle() + "]]></Description>" +
                    "<PicUrl><![CDATA[" + course.getCover() + "]]></PicUrl>" +
                    "<Url><![CDATA[http://smartplanetmobile.free.idcfengye.com/#/live/info/" + course.getId() + "]]></Url>" +
                    "</item>";

            text.append("<xml>");
            text.append("<ToUserName><![CDATA[").append(fromUsername).append("]]></ToUserName>");
            text.append("<FromUserName><![CDATA[").append(toUsername).append("]]></FromUserName>");
            text.append("<CreateTime><![CDATA[").append(createTime).append("]]></CreateTime>");
            text.append("<MsgType><![CDATA[news]]></MsgType>");
            text.append("<ArticleCount><![CDATA[1]]></ArticleCount>");
            text.append("<Articles>");
            text.append(articles);
            text.append("</Articles>");
            text.append("</xml>");
        }

        return text.toString();
    }

    /**
     * 回复文本
     *
     * @param param   请求参数
     * @param content 上下文
     * @return message 消息
     */
    private StringBuffer text(Map<String, String> param, String content)
    {
        String fromUsername = param.get("FromUserName");
        String toUsername = param.get("ToUserName");

        // 单位为秒,不是毫秒
        long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();

        text.append("<xml>");
        text.append("<ToUserName><![CDATA[").append(fromUsername).append("]]></ToUserName>");
        text.append("<FromUserName><![CDATA[").append(toUsername).append("]]></FromUserName>");
        text.append("<CreateTime><![CDATA[").append(createTime).append("]]></CreateTime>");
        text.append("<MsgType><![CDATA[text]]></MsgType>");
        text.append("<Content><![CDATA[").append(content).append("]]></Content>");
        text.append("</xml>");

        return text;
    }

    /**
     * 处理关注事件
     *
     * @param param 请求参数
     * @return message 消息
     */
    private String subscribe(Map<String, String> param)
    {
        return this.text(param, "感谢你关注“智慧星球”,可以根据关键字搜索你想看的视频教程,如:Java 基础、Spring Boot、大数据等。").toString();
    }

    /**
     * 处理取消关注事件
     *
     * @param param 请求参数
     * @return message 消息
     */
    private String unsubscribe(Map<String, String> param)
    {
        return "success";
    }

    /**
     * 关于我们
     *
     * @param param 请求参数
     * @return message 消息
     */
    private String aboutUs(Map<String, String> param)
    {
        StringBuffer message = this.text(param, "智慧星球现开设 Java、HTML5 前端 + 全栈、大数据、全链路 UI/UE 设计、人工智能、大数据运维 + Python 自动化、Android + HTML5 混合开发等多门课程;" +
                "同时,通过视频分享、智慧星球在线课堂、大厂学苑直播课堂等多种方式,满足了全国编程爱好者对多样化学习场景的需求,已经为行业输送了大量 IT 技术人才。");

        return message.toString();
    }
}
4.5、更改 MessageController 方法
package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.wechat.service.MessageService;
import jakarta.servlet.http.HttpServletRequest;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

/**
 * @author MYXH
 * @date 2023/10/17
 */
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController
{
    @Autowired
    private MessageService messageService;

    /**
     * 接收微信服务器发送来的消息
     *
     * @param request 请求
     * @return message 消息
     * @throws Exception 异常
     */
    @PostMapping
    public String receiveMessage(HttpServletRequest request) throws Exception
    {
        /*
        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
        System.out.println(JSONObject.toJSONString(wxMpXmlMessage));

        return "success";
         */

        Map<String, String> param = this.parseXml(request);
        String message = messageService.receiveMessage(param);

        return message;
    }

    private Map<String, String> parseXml(HttpServletRequest request) throws Exception
    {
        Map<String, String> map = new HashMap<>();
        InputStream inputStream = request.getInputStream();
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        Element root = document.getRootElement();
        List<Element> elementList = root.elements();

        for (Element e : elementList)
        {
            map.put(e.getName(), e.getText());
        }

        inputStream.close();
        inputStream = null;

        return map;
    }
}
5、测试公众号消息

(1)点击个人 -> 关于我们,返回关于我们的介绍。

测试公众号消息

(2)在公众号输入关键字,返回搜索的课程信息。

测试公众号消息

二、公众号模板消息

1、实现目标

购买课程支付成功微信推送消息。

2、模板消息实现

接口文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html

3、申请模板消息

首先需要知道,模板消息是需要申请的。

但是在申请时还是有一些东西要注意,这个在官方的文档有非常详细的说明。

https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Operation_Specifications.html

申请模板消息

这个好好看看。选择行业的时候可要谨慎些,因为这个一个月只可以修改一次。

下面看看在哪里申请,智慧星球已经申请过,忽略。

申请模板消息

申请之后就耐心等待,审核通过之后就会出现“广告与服务”模板消息的菜单。

申请模板消息

4、添加模板消息

审核通过之后,就可以添加模板消息,进行开发了。

点击模板消息进入后,直接在模板库中选择你需要的消息模板添加就可以了,添加之后就会在我的模板中。会有一个模板 id,这个模板 id 在发送消息的时候会用到。

模板消息如下:

添加模板消息

需要模板消息:

​1、订单支付成功通知。

模板库中没有的模板,可以自定义模板,审核通过后可以使用。

5、公众号测试号申请模板消息
5.1、新增测试模板

新增测试模板

5.2、填写信息

(1)下载示例参考。

下载地址:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Operation_Specifications.html

下载示例参考

下载示例参考

(2)填写模板标题和模板内容。

填写模板标题和模板内容

6、模板消息接口封装
6.1、MessageController

添加方法。

package com.myxh.smart.planet.wechat.controller;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.wechat.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController
{
    @Autowired
    private MessageService messageService;

    /**
     * 推送支付消息
     *
     * @return Result 全局统一返回结果
     */
    @GetMapping("/push/pay/message")
    public Result<Void> pushPayMessage()
    {
        messageService.pushPayMessage(1L);

        return Result.ok(null);
    }
}
6.2、service 接口

MessageService

package com.myxh.smart.planet.wechat.service;

/**
 * @author MYXH
 * @date 2023/10/17
 */
public interface MessageService
{
    /**
     * 推送支付消息,订单成功
     */
    void pushPayMessage(long orderId);
}
6.3、service 接口实现

(1)MessageServiceImpl 类。

(2)openid 值。

openid 值

(3)模板 id 值。

模板 id 值

package com.myxh.smart.planet.wechat.service.impl;

import com.myxh.smart.planet.wechat.service.MessageService;
import lombok.SneakyThrows;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@Service
public class MessageServiceImpl implements MessageService
{
    @Autowired
    private WxMpService wxMpService;

    /**
     * 推送支付消息,订单成功
     * 暂时写成固定值测试,后续完善
     */
    @SneakyThrows
    @Override
    public void pushPayMessage(long orderId)
    {
        // 微信 openid
        String openid = "oxM4d64iKq9SD6lduBKcF4MQTjF8";
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                // 要推送的用户 openid
                .toUser(openid)
                // 模板 id
                .templateId("URQAUtz9-IrYVFwsuI5Ul4pKUcRFKGmOTwRAb6lscJM")
                // 点击模板消息要访问的网址
                .url("http://smartplanet.free.idcfengye.com/#/pay/" + orderId)
                .build();

        // 如果是正式版发送消息,这里需要配置信息
        templateMessage.addData(new WxMpTemplateData("first", "亲爱的用户:您有一笔订单支付成功。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", "20231123180908744", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", "Java基础课程", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword3", "2023-11-23", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword4", "100", "#272727"));
        templateMessage.addData(new WxMpTemplateData("remark", "感谢你购买课程,如有疑问,随时咨询!", "#272727"));
        String message = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        System.out.println(message);
    }
}
6.4、通过 swagger 测试效果

(1)在公众号可以看到发送的模板消息。

通过 swagger 测试效果

三、微信授权登录

1、需求描述

根据流程图通过菜单进入的页面都要授权登录

需求描述

2、授权登录

接口文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

说明:

​1、严格按照接口文档实现。

2、应用授权作用域 scope:scope 为 snsapi_userinfo。

2.1、配置授权回调域名

(1)在公众号正式号配置。

在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“设置与开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是 URL,因此请勿加 http:// 等协议头。

本地测试配置内网穿透地址。

在公众号正式号配置

(2)在公众号测试号配置。

在公众号测试号配置

在公众号测试号配置

2.2、部署公众号前端页面

(1)公众号前端页面已经开发完成,直接部署使用即可。

部署公众号前端页面

(2)启动公众号页面项目

使用命令:npm run serve。

部署公众号前端页面

2.3、前端处理

(1)全局处理授权登录,处理页面:/src/App.vue。

说明 1:访问页面时首先判断是否有 token 信息,如果没有跳转到授权登录接口。

说明 2:通过 localStorage 存储 token 信息。

在 HTML5 中,加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了 cookie 存储空间不足的问题(cookie 中每条 cookie 的存储空间很小,只有几 K),localStorage 中一般浏览器支持的是 5M 大小,这个在不同的浏览器中 localStorage 会有所不同。它只能存储字符串格式的数据,所以最好在每次存储时把数据转换成 json 格式,取出的时候再转换回来。

(2)前端代码实现。

<template>
  <div id="app">
    <div id="nav">
      <!-- <router-link to="/">列表页</router-link> | -->
      <!-- <router-link to="/info">详情页</router-link> | -->
      <!-- <router-link to="/list">列表页</router-link> | -->
      <!-- <router-link to="/order">下单页</router-link> -->
      <van-button round block type="info" @click="clearData"
        >清空 localStorage</van-button
      >
    </div>
    <router-view />
  </div>
</template>

<script>
import userInfoAPI from "@/api/userInfo";

export default {
  data() {
    return {
      show: true,
    };
  },

  created() {
    // 处理微信授权登录
    this.wechatLogin();
  },

  methods: {
    wechatLogin() {
      // 处理微信授权登录
      let token = this.getQueryString("token") || "";

      if (token != "") {
        window.localStorage.setItem("token", token);
      }

      // 所有页面都必须登录,两次调整登录,这里与接口返回 208 状态
      token = window.localStorage.getItem("token") || "";

      if (token == "") {
        let url = window.location.href.replace("#", "smartplanet");
        window.location =
          "http://smartplanet.free.idcfengye.com/api/user/wechat/authorize?returnUrl=" +
          url;
      }

      console.log("token:" + window.localStorage.getItem("token"));

      //绑定手机号处理
      /*
      if (token != "") {
        this.bindPhone();
      }
       */
    },

    bindPhone() {
      let userInfoString = window.localStorage.getItem("userInfo") || "";
      alert("userInfoString:" + userInfoString);

      if (userInfoString != "") {
        alert("userInfoString:" + userInfoString);
        let userInfo = JSON.parse(userInfoString);
        let phone = userInfo.phone || "";

        if (phone == "") {
          this.$router.push({ path: "/bindFirst" });
        }
      } else {
        alert("userInfoString:" + userInfoString);
        userInfoAPI.getCurrentUserInfo().then((response) => {
          window.localStorage.setItem(
            "userInfo",
            JSON.stringify(response.data)
          );
          alert("data:" + JSON.stringify(response.data));
          let phone = response.data.phone || "";
          console.log("phone:" + phone);

          if (phone == "") {
            this.$router.push({ path: "/bindFirst" });
          }
        });
      }
    },

    getQueryString(paramName) {
      if (window.location.href.indexOf("?") == -1) return "";

      let searchString = window.location.href.split("?")[1];
      let i,
        val,
        params = searchString.split("&");

      for (i = 0; i < params.length; i++) {
        val = params[i].split("=");

        if (val[0] == paramName) {
          return val[1];
        }
      }

      return "";
    },

    clearData() {
      window.localStorage.setItem("token", "");
      window.localStorage.setItem("userInfo", "");
      let token = window.localStorage.getItem("token");
      alert("token:" + token);
    },
  },
};
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>
3、授权登录接口

操作模块:service-user。

3.1、引入微信工具包
<!-- aliyun-java-sdk-core -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
</dependency>

<!-- weixin-java-mp -->
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.5.0</version>
</dependency>

<!-- dom4j -->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.4</version>
</dependency>
3.2、添加配置
# 公众号 id 和秘钥
# 智慧星球微信公众平台 appId
wechat.appId=wxc23b80b9ffaac7bd
# 智慧星球微信公众平台 api 秘钥
wechat.appSecret=5c0271622c4271753310c436b5cd3532

# 授权回调获取用户信息接口地址
wechat.userInfoUrl: http://smartplanet.free.idcfengye.com/api/user/wechat/user/info
3.3、添加工具类
package com.myxh.smart.planet.user.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/18
 * @description 常量类,读取配置文件 application.properties 中的配置
 */
@Component
public class ConstantPropertiesUtil implements InitializingBean
{
    @Value("${wechat.appId}")
    private String appId;

    @Value("${wechat.appSecret}")
    private String appSecret;

    public static String ACCESS_KEY_ID;
    public static String ACCESS_KEY_SECRET;

    @Override
    public void afterPropertiesSet() throws Exception
    {
        ACCESS_KEY_ID = appId;
        ACCESS_KEY_SECRET = appSecret;
    }
}
package com.myxh.smart.planet.user.config;

import com.myxh.smart.planet.user.utils.ConstantPropertiesUtil;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/18
 */
@Component
public class WeChatMpConfig
{
    @Autowired
    public WeChatMpConfig(ConstantPropertiesUtil constantPropertiesUtil)
    {

    }

    @Bean
    public WxMpService wxMpService(WxMpConfigStorage wxMpConfigStorage)
    {
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage);

        return wxMpService;
    }

    @Bean
    public WxMpConfigStorage wxMpConfigStorage()
    {
        WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
        wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
        wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);

        return wxMpConfigStorage;
    }
}
3.4、controller 类
package com.myxh.smart.planet.user.api;

import com.alibaba.fastjson2.JSON;
import com.myxh.smart.planet.jwt.JwtHelper;
import com.myxh.smart.planet.model.user.UserInfo;
import com.myxh.smart.planet.user.service.UserInfoService;
import jakarta.servlet.http.HttpServletRequest;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * @author MYXH
 * @date 2023/10/18
 */
@Controller
@RequestMapping("/api/user/wechat")
public class WechatController
{
    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private WxMpService wxMpService;

    @Value("${wechat.userInfoUrl}")
    private String userInfoUrl;

    /**
     * 授权跳转
     *
     * @param returnUrl 返回 url
     * @param request   请求
     * @return redirectURL 重定向 url
     */
    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request)
    {
        String redirectURL = wxMpService.getOAuth2Service().buildAuthorizationUrl(userInfoUrl,
                WxConsts.OAuth2Scope.SNSAPI_USERINFO,
                URLEncoder.encode(returnUrl.replace("#", "smartplanet"), StandardCharsets.UTF_8));

        return "redirect:" + redirectURL;
    }

    /**
     * 获取用户信息
     *
     * @param code      密码
     * @param returnUrl 返回 url
     * @return redirectURL 重定向 url
     */
    @GetMapping("/user/info")
    public String userInfo(@RequestParam("code") String code,
                           @RequestParam("state") String returnUrl)
    {
        try
        {
            // 拿着 code 发送请求
            WxOAuth2AccessToken wxOAuth2AccessToken = this.wxMpService.getOAuth2Service().getAccessToken(code);

            // 获取 openId
            String openId = wxOAuth2AccessToken.getOpenId();
            System.out.println("微信网页授权 openId = " + openId);

            // 获取微信信息
            WxOAuth2UserInfo wxOAuth2UserInfo = wxMpService.getOAuth2Service().getUserInfo(wxOAuth2AccessToken, null);
            System.out.println("微信网页授权 wxOAuth2UserInfo = " + JSON.toJSONString(wxOAuth2UserInfo));

            // 获取微信信息添加到数据库
            UserInfo userInfo = userInfoService.getUserInfoByOpenid(openId);

            if (userInfo == null)
            {
                userInfo = new UserInfo();
                userInfo.setOpenId(openId);
                userInfo.setUnionId(wxOAuth2UserInfo.getUnionId());
                userInfo.setNickName(wxOAuth2UserInfo.getNickname());
                userInfo.setAvatar(wxOAuth2UserInfo.getHeadImgUrl());
                userInfo.setSex(wxOAuth2UserInfo.getSex());
                userInfo.setProvince(wxOAuth2UserInfo.getProvince());
                userInfoService.save(userInfo);
            }

            // 生成 token,按照一定规则生成字符串,可以包含用户信息
            String token = JwtHelper.createToken(userInfo.getId(), userInfo.getNickName());

            // 授权完成之后,跳转具体功能页面
            if (!returnUrl.contains("?"))
            {
                return "redirect:" + returnUrl + "?token=" + token;
            }
            else
            {
                return "redirect:" + returnUrl + "&token=" + token;
            }
        }
        catch (WxErrorException e)
        {
            e.printStackTrace();
        }

        return null;
    }
}
3.5、编写 UserInfoService
package com.myxh.smart.planet.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.user.UserInfo;
import com.myxh.smart.planet.user.mapper.UserInfoMapper;
import com.myxh.smart.planet.user.service.UserInfoService;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/15
 *
 * <p>
 * 用户信息 服务实现类
 * </p>
 */
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService
{
    /**
     * 根据 openId 获取用户信息
     *
     * @param openId openId openId
     * @return userInfo 用户信息
     */
    @Override
    public UserInfo getUserInfoByOpenid(String openId)
    {
        QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
        wrapper.eq("open_id", openId);
        UserInfo userInfo = baseMapper.selectOne(wrapper);

        return userInfo;
    }
}
3.6、使用 token

通过 token 传递用户信息。

3.6.1、JWT 介绍

JWT 工具。

JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准。

JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上

JWT 最重要的作用就是对 token 信息的防伪作用。

3.6.2、JWT 的原理

一个 JWT 由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行 base64 编码得到 JWT。

JWT 的原理

(1)公共部分。

主要是该 JWT 的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。

(2)私有部分。

用户自定义的内容,根据实际需要真正要封装的信息。

userInfo{用户的 Id,用户的昵称 nickName}。

(3)签名部分。

SaltiP: 当前服务器的 IP 地址{linux 中配置代理服务器的 ip}。

主要用户对 JWT 生成字符串的时候,进行加密{盐值}。

base64 编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把 base64 编码解成明文,所以不要在 JWT 中放入涉及私密的信息。

3.6.3、整合 JWT

(1)在 service-utils 模块添加依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.myxh.smart.planet</groupId>
        <artifactId>common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>service-utils</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

        <!-- jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
        </dependency>

        <!-- jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- jjwt-jackson -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 日期时间工具 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>
    </dependencies>
</project>

(2)添加 JWT 工具类。

package com.myxh.smart.planet.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.util.ObjectUtils;

import java.util.Date;

/**
 * @author MYXH
 * @date 2023/10/19
 */
public class JwtHelper
{
    // token 字符串有效时间
    private static final long tokenExpiration = 24 * 60 * 60 * 1000;

    // 加密编码秘钥
    private static final String tokenSignKey = "SmartPlanetSmartPlanetSmartPlanetSmartPlanetSmartPlanetSmartPlanet";

    /**
     * 根据 userId 和 username 生成 token 字符串
     *
     * @param userId   用户 id
     * @param userName 用户姓名
     * @return token 字符串
     */
    public static String createToken(Long userId, String userName)
    {
        String token = Jwts.builder()
                // 设置 token 分类
                .setSubject("SmartPlanet-USER")

                // token 字符串有效时长
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))

                // 私有部分(用户信息)
                .claim("userId", userId)
                .claim("userName", userName)

                // 根据秘钥使用加密编码方式进行加密,对字符串压缩
                .signWith(Keys.hmacShaKeyFor(tokenSignKey.getBytes()), SignatureAlgorithm.HS512)
                .compressWith(CompressionCodecs.GZIP)
                .compact();

        return token;
    }

    /**
     * 从 token字符串获取 userId
     *
     * @param token token字符串
     * @return 用户 id
     */
    public static Long getUserId(String token)
    {
        if (ObjectUtils.isEmpty(token))
        {
            return null;
        }

        Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(tokenSignKey.getBytes()).build().parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        Integer userId = (Integer) claims.get("userId");

        return userId.longValue();
    }

    /**
     * 从 token 字符串获取 userName
     *
     * @param token token字符串
     * @return userName 用户姓名
     */
    public static String getUserName(String token)
    {
        if (ObjectUtils.isEmpty(token))
        {
            return "";
        }

        Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(tokenSignKey.getBytes()).build().parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        String userName = (String) claims.get("userName");

        return userName;
    }

    public static void main(String[] args)
    {
        String token = JwtHelper.createToken(1L, "MYXH");
        System.out.println("token = " + token);
        System.out.println("JwtHelper.getUserId(token) = " + JwtHelper.getUserId(token));
        System.out.println("JwtHelper.getUserName(token) = " + JwtHelper.getUserName(token));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末影小黑xh

感谢朋友们对我的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值