基于 Spring Boot 和 UniApp 实现微信小程序消息通知

基于 Spring Boot 和 UniApp 实现微信小程序消息通知

一、前言

在现代移动应用开发中,消息通知功能是提升用户体验和应用交互性的重要手段。微信小程序提供了丰富的消息推送能力,而 UniApp 作为一款跨平台的前端开发框架,能够方便地与微信小程序集成。本文将详细介绍如何基于 Spring Boot 实现 UniApp 微信小程序的消息通知功能,包括后端服务的搭建、微信小程序的配置以及前端页面的实现。我们将通过一个实际案例,展示如何推送订单状态变更通知。

二、技术栈

  • 后端:Spring Boot
  • 前端:UniApp
  • 消息推送服务:微信小程序订阅消息
  • 开发工具:IntelliJ IDEA、HBuilderX

三、实现步骤

(一)后端服务搭建

  1. 创建 Spring Boot 项目
    • 使用 Spring Initializr(/)快速生成项目,选择以下依赖:
      • Spring Web
      • Lombok(可选,用于简化代码)
    • 下载并解压项目,导入到 IntelliJ IDEA 中。
  2. 引入微信小程序相关依赖
    • pom.xml 文件中添加微信小程序的 Java SDK 依赖:
代码语言:xml复制
     <dependency>
         <groupId>com.github.binarywang</groupId>
         <artifactId>weixin-java-miniapp</artifactId>
         <version>4.6.0</version>
     </dependency>
  1. 配置微信小程序参数
    • application.yml 文件中添加微信小程序的配置信息:
代码语言:yaml复制
     wechat:
       miniapp:
         appid: wxxxxxxxxxxxxxx
         secret: xxxxxxxxxxxxxxxx
         token: xxxxxxxxxxxxxxxx
         aes-key: xxxxxxxxxxxxxxxx
  1. 实现微信消息推送服务
    • 创建一个服务类 WxMaService,用于封装微信消息推送的逻辑:
代码语言:java复制
     import com.github.binarywang.wxpay.config.WxPayConfig;
     import com.github.binarywang.wxpay.service.WxPayService;
     import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
     import com.github.binarywang.weixin.mp.config.WxMpConfigStorage;
     import com.github.binarywang.weixin.mp.config.impl.WxMpDefaultConfigImpl;
     import com.github.binarywang.weixin.mp.service.WxMpService;
     import com.github.binarywang.weixin.mp.service.impl.WxMpServiceImpl;
     import com.github.binarywang.weixin.sdk.mp.bean.template.WxMpTemplateMessage;
     import org.springframework.beans.factory.annotation.Value;
     import org.springframework.stereotype.Service;

     @Service
     public class WxMaService {
         @Value("${wechat.miniapp.appid}")
         private String appId;
         @Value("${wechat.miniapp.secret}")
         private String secret;

         public void sendTemplateMessage(String openId, String templateId, String page, String formId, String data) {
             WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
             config.setAppId(appId);
             config.setSecret(secret);

             WxMpService wxMpService = new WxMpServiceImpl();
             wxMpService.setWxMpConfigStorage(config);

             WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                     .toUser(openId)
                     .templateId(templateId)
                     .page(page)
                     .formId(formId)
                     .data(data)
                     .build();

             try {
                 wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
     }
  1. 创建消息推送接口
    • 创建一个控制器类 MessageController,提供一个接口用于触发消息推送:
代码语言:java复制
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestParam;
     import org.springframework.web.bind.annotation.RestController;

     @RestController
     public class MessageController {
         @Autowired
         private WxMaService wxMaService;

         @GetMapping("/sendOrderNotification")
         public String sendOrderNotification(@RequestParam String openId, @RequestParam String orderId) {
             String templateId = "YOUR_TEMPLATE_ID"; // 替换为实际的模板ID
             String page = "/pages/orderDetail/orderDetail?orderId=" + orderId;
             String formId = "YOUR_FORM_ID"; // 替换为实际的FormID
             String data = "{\"keyword1\": {\"value\": \"订单号:" + orderId + "\"}, \"keyword2\": {\"value\": \"订单状态:已发货\"}}";

             wxMaService.sendTemplateMessage(openId, templateId, page, formId, data);
             return "消息已发送";
         }
     }

(二)前端页面开发

  1. 创建 UniApp 项目
    • 使用 HBuilderX 创建一个新的 UniApp 项目,选择微信小程序模板。
  2. 实现订阅消息功能
    • 在页面中添加一个按钮,用于触发订阅消息:
代码语言:vue复制
     <template>
         <view class="container">
             <button @click="subscribeMessage">订阅消息</button>
         </view>
     </template>

     <script>
     export default {
         data() {
             return {
                 openId: null,
             };
         },
         methods: {
             async subscribeMessage() {
                 const templateIds = [
                     "YOUR_TEMPLATE_ID", // 替换为实际的模板ID
                 ];

                 const res = await uni.requestSubscribeMessage({
                     tmplIds: templateIds,
                     success: (res) => {
                         console.log("订阅成功", res);
                         if (res[templateIds[0]] === "accept") {
                             uni.showToast({
                                 title: "订阅成功",
                                 icon: "success",
                                 duration: 2000,
                             });
                         } else {
                             uni.showToast({
                                 title: "您已拒绝订阅",
                                 icon: "none",
                                 duration: 2000,
                             });
                         }
                     },
                     fail: (err) => {
                         console.error("订阅失败", err);
                         uni.showToast({
                             title: "订阅失败",
                             icon: "none",
                             duration: 2000,
                         });
                     },
                 });
             },
         },
     };
     </script>

     <style scoped>
     .container {
         display: flex;
         justify-content: center;
         align-items: center;
         height: 100vh;
     }
     button {
         width: 200px;
         height: 50px;
         line-height: 50px;
         text-align: center;
         background-color: #1aad19;
         color: white;
         font-size: 16px;
         border-radius: 5px;
     }
     </style>
     ```

3. **获取用户 `openId`**
   - 在页面中添加一个方法用于获取 `openId`:

     ```vue
     <script>
     export default {
         data() {
             return {
                 openId: null,
             };
         },
         methods: {
             async getOpenId() {
                 const res = await uni.login();
                 const code = res.code;

                 // 调用后端接口,通过code换取openId
                 uni.request({
                     url: "http://your-backend-server/getOpenId", // 替换为实际的后端接口
                     method: "POST",
                     data: {
                         code: code,
                     },
                     success: (res) => {
                         if (res.data.success) {
                             this.openId = res.data.openId;
                             uni.showToast({
                                 title: "获取openId成功",
                                 icon: "success",
                                 duration: 2000,
                             });
                         } else {
                             uni.showToast({
                                 title: "获取openId失败",
                                 icon: "none",
                                 duration: 2000,
                             });
                         }
                     },
                     fail: (err) => {
                         console.error("获取openId失败", err);
                         uni.showToast({
                             title: "获取openId失败",
                             icon: "none",
                             duration: 2000,
                         });
                     },
                 });
             },
         },
     };
     </script>
  1. 后端接口用于通过 code 换取 openId
    • 在后端创建一个接口用于通过 code 换取 openId
代码语言:java复制
     import com.github.binarywang.weixin.mp.api.WxMpService;
     import com.github.binarywang.weixin.mp.api.impl.WxMpServiceImpl;
     import com.github.binarywang.weixin.mp.config.WxMpDefaultConfigImpl;
     import org.springframework.web.bind.annotation.PostMapping;
     import org.springframework.web.bind.annotation.RequestBody;
     import org.springframework.web.bind.annotation.RestController;

     import java.util.Map;

     @RestController
     public class OpenIdController {
         @PostMapping("/getOpenId")
         public Map<String, Object> getOpenId(@RequestBody Map<String, String> requestBody) {
             String code = requestBody.get("code");

             WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
             config.setAppId("YOUR_APP_ID");
             config.setSecret("YOUR_APP_SECRET");

             WxMpService wxMpService = new WxMpServiceImpl();
             wxMpService.setWxMpConfigStorage(config);

             try {
                 String openId = wxMpService.getUserService().getOpenId(code);
                 return Map.of("success", true, "openId", openId);
             } catch (Exception e) {
                 e.printStackTrace();
                 return Map.of("success", false, "message", "获取openId失败");
             }
         }
     }

四、完整代码示例

后端代码

代码语言:java复制
// WxMaService.java
import com.github.binarywang.weixin.mp.api.WxMpService;
import com.github.binarywang.weixin.mp.api.impl.WxMpServiceImpl;
import com.github.binarywang.weixin.mp.config.WxMpDefaultConfigImpl;
import com.github.binarywang.weixin.mp.bean.template.WxMpTemplateMessage;
import org.springframework.stereotype.Service;

@Service
public class WxMaService {
    private final String appId = "YOUR_APP_ID";
    private final String secret = "YOUR_APP_SECRET";

    public void sendTemplateMessage(String openId, String templateId, String page, String formId, String data) {
        WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
        config.setAppId(appId);
        config.setSecret(secret);

        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(config);

        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                .toUser(openId)
                .templateId(templateId)
                .page(page)
                .formId(formId)
                .data(data)
                .build();

        try {
            wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
代码语言:java复制
// MessageController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {
    @Autowired
    private WxMaService wxMaService;

    @GetMapping("/sendOrderNotification")
    public String sendOrderNotification(@RequestParam String openId, @RequestParam String orderId) {
        String templateId = "YOUR_TEMPLATE_ID";
        String page = "/pages/orderDetail/orderDetail?orderId=" + orderId;
        String formId = "YOUR_FORM_ID";
        String data = "{\"keyword1\": {\"value\": \"订单号:" + orderId + "\"}, \"keyword2\": {\"value\": \"订单状态:已发货\"}}";

        wxMaService.sendTemplateMessage(openId, templateId, page, formId, data);
        return "消息已发送";
    }
}
代码语言:java复制
// OpenIdController.java
import com.github.binarywang.weixin.mp.api.WxMpService;
import com.github.binarywang.weixin.mp.api.impl.WxMpServiceImpl;
import com.github.binarywang.weixin.mp.config.WxMpDefaultConfigImpl;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class OpenIdController {
    @PostMapping("/getOpenId")
    public Map<String, Object> getOpenId(@RequestBody Map<String, String> requestBody) {
        String code = requestBody.get("code");

        WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
        config.setAppId("YOUR_APP_ID");
        config.setSecret("YOUR_APP_SECRET");

        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(config);

        try {
            String openId = wxMpService.getUserService().getOpenId(code);
            return Map.of("success", true, "openId", openId);
        } catch (Exception e) {
            e.printStackTrace();
            return Map.of("success", false, "message", "获取openId失败");
        }
    }
}

前端代码

代码语言:vue复制
<template>
  <view class="container">
    <button @click="subscribeMessage">订阅消息</button>
    <button @click="getOpenId">获取openId</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      openId: null,
    };
  },
  methods: {
    async subscribeMessage() {
      const templateIds = [
        "YOUR_TEMPLATE_ID", // 替换为实际的模板ID
      ];

      const res = await uni.requestSubscribeMessage({
        tmplIds: templateIds,
        success: (res) => {
          console.log("订阅成功", res);
          if (res[templateIds[0]] === "accept") {
            uni.showToast({
              title: "订阅成功",
              icon: "success",
              duration: 2000,
            });
          } else {
            uni.showToast({
              title: "您已拒绝订阅",
              icon: "none",
              duration: 2000,
            });
          }
        },
        fail: (err) => {
          console.error("订阅失败", err);
          uni.showToast({
            title: "订阅失败",
            icon: "none",
            duration: 2000,
          });
        },
      });
    },
    async getOpenId() {
      const res = await uni.login();
      const code = res.code;

      // 调用后端接口,通过code换取openId
      uni.request({
        url: "http://your-backend-server/getOpenId", // 替换为实际的后端接口
        method: "POST",
        data: {
          code: code,
        },
        success: (res) => {
          if (res.data.success) {
            this.openId = res.data.openId;
            uni.showToast({
              title: "获取openId成功",
              icon: "success",
              duration: 2000,
            });
          } else {
            uni.showToast({
              title: "获取openId失败",
              icon: "none",
              duration: 2000,
            });
          }
        },
        fail: (err) => {
          console.error("获取openId失败", err);
          uni.showToast({
            title: "获取openId失败",
            icon: "none",
            duration: 2000,
          });
        },
      });
    },
  },
};
</script>

<style scoped>
.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
button {
  width: 200px;
  height: 50px;
  line-height: 50px;
  text-align: center;
  background-color: #1aad19;
  color: white;
  font-size: 16px;
  border-radius: 5px;
  margin: 10px 0;
}
</style>

五、完整流程整合

1. 用户订阅消息

  • 用户进入小程序页面,点击“订阅消息”按钮。
  • 调用 uni.requestSubscribeMessage 方法,请求用户订阅消息。
  • 如果用户同意订阅,将模板ID存储到本地或后端。2. 获取用户 openId
  • 用户进入小程序页面时,自动调用 uni.login 获取 code
  • code 发送到后端,后端通过微信API换取 openId
  • openId 存储到本地或后端。

3. 后端触发消息推送

  • 当需要推送消息时,后端调用 WxMaService.sendTemplateMessage 方法。
  • 指定 openId、模板ID、页面路径、表单ID和消息内容。
  • 微信小程序收到消息后,用户点击消息即可跳转到指定页面。

六、注意事项

1. 模板消息限制

  • 微信模板消息有严格的使用限制,例如每天只能发送一定数量的消息。
  • 模板消息的内容需要符合微信的要求,不能包含广告或营销信息。

2. 表单ID获取

  • 表单ID是通过用户在小程序中提交表单时获取的。
  • 每个表单ID只能使用一次,使用后需要重新获取。

3. 安全性

  • 在后端处理微信消息推送时,需要验证用户的身份,确保消息推送给正确的用户。
  • 对于敏感信息,如 openIdformId,需要进行加密存储和传输。七、总结

本文详细介绍了如何基于 Spring Boot 和 UniApp 实现微信小程序的消息通知功能。通过后端服务的搭建、微信小程序的配置以及前端页面的实现,我们成功实现了一个完整的消息推送流程。在实际开发中,可以根据业务需求进一步扩展和优化代码,例如增加消息推送的重试机制、优化用户体验等。希望本文对大家有所帮助!

八、扩展功能

1. 消息推送重试机制

在实际业务中,可能会遇到消息推送失败的情况。可以通过以下方式实现消息推送的重试机制:

  • WxMaService 中增加重试逻辑:
代码语言:java复制
  public void sendTemplateMessage(String openId, String templateId, String page, String formId, String data) {
      WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();
      config.setAppId(appId);
      config.setSecret(secret);

      WxMpService wxMpService = new WxMpServiceImpl();
      wxMpService.setWxMpConfigStorage(config);

      WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
              .toUser(openId)
              .templateId(templateId)
              .page(page)
              .formId(formId)
              .data(data)
              .build();

      int retryCount = 3; // 重试次数
      for (int i = 0; i < retryCount; i++) {
          try {
              wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
              break; // 发送成功,退出循环
          } catch (Exception e) {
              e.printStackTrace();
              if (i == retryCount - 1) {
                  throw new RuntimeException("消息推送失败,重试次数已达上限");
              }
          }
      }
  }

2. 用户订阅状态管理

可以将用户的订阅状态存储到数据库中,以便在需要时查询用户是否已订阅相关消息:

  • 创建一个用户订阅状态表:
代码语言:sql复制
  CREATE TABLE user_subscription (
      id BIGINT AUTO_INCREMENT PRIMARY KEY,
      open_id VARCHAR(255) NOT NULL,
      template_id VARCHAR(255) NOT NULL,
      subscribed BOOLEAN DEFAULT FALSE,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  • 在后端更新用户订阅状态:
代码语言:java复制
  @PostMapping("/updateSubscription")
  public Map<String, Object> updateSubscription(@RequestBody Map<String, String> requestBody) {
      String openId = requestBody.get("openId");
      String templateId = requestBody.get("templateId");
      boolean subscribed = requestBody.get("subscribed").equals("true");
      // 更新数据库中的订阅状态
      // 示例代码,具体实现根据实际数据库操作框架
      userSubscriptionService.updateSubscription(openId, templateId, subscribed);

      return Map.of("success", true);
  }

3. 消息内容动态生成

在实际业务中,消息内容可能需要根据业务逻辑动态生成。可以通过模板引擎(如 Thymeleaf 或 Velocity)来实现消息内容的动态生成:

  • 创建一个模板文件 messageTemplate.html
代码语言:html复制
  <div>
      <p>尊敬的用户,您的订单 {{orderId}} 已发货。</p>
      <p>发货时间:{{shipTime}}</p>
  </div>
  • 在后端生成消息内容:
代码语言:java复制
  import org.thymeleaf.TemplateEngine;
  import org.thymeleaf.context.Context;
  import org.thymeleaf.spring5.SpringTemplateEngine;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.stereotype.Service;
  @Service
  public class MessageContentService {
      @Autowired
      private SpringTemplateEngine templateEngine;

      public String generateMessageContent(String orderId, String shipTime) {
          Context context = new Context();
          context.setVariable("orderId", orderId);
          context.setVariable("shipTime", shipTime);

          return templateEngine.process("messageTemplate", context);
      }
  }

九、常见问题及解决方案

1. 消息推送失败

  • 原因:可能是 openIdformId 错误,或者模板消息内容不符合微信要求。
  • 解决方案
    • 确保 openIdformId 是正确的。
    • 检查模板消息内容是否符合微信的要求,避免包含敏感词汇或广告内容。
    • 检查微信小程序的后台配置是否正确。

2. 用户拒绝订阅消息

  • 原因:用户可能不希望接收消息,或者对消息内容不感兴趣。
  • 解决方案
    • 提供清晰的订阅提示,让用户了解订阅消息的用途和好处。
    • 确保消息内容有价值,避免发送无关紧要的信息。
    • 提供用户取消订阅的选项,提升用户体验。

3. 表单ID不足

  • 原因:表单ID是通过用户提交表单时获取的,如果用户长时间不提交表单,可能会导致表单ID不足。
  • 解决方案
    • 在用户提交表单时,及时获取并存储表单ID。
    • 如果表单ID不足,可以提示用户重新提交表单以获取新的表单ID。
    • 合理规划表单ID的使用,避免频繁发送消息。

十、总结与展望

通过本文的介绍,我们已经完整地实现了基于 Spring Boot 和 UniApp 的微信小程序消息通知功能。从后端服务的搭建到前端页面的实现,再到消息推送的完整流程,我们详细展示了每一步的操作步骤和代码示例。在实际开发中,可以根据业务需求进一步扩展和优化代码,例如增加消息推送的重试机制、优化用户体验等。

未来,随着微信小程序功能的不断更新和优化,消息通知功能也将有更多的扩展可能性。例如,结合微信支付、微信客服消息等功能,可以进一步提升小程序的用户体验和业务价值。希望本文能够为开发者提供有价值的参考,帮助大家更好地实现微信小程序的消息通知功能。

如果你有任何问题或建议,欢迎在评论区留言,我会及时回复!