手把手教你接入抖音小程序发送模板消息通知

本文介绍模板消息的概念及其在字节跳动平台的实现方式,包括配置流程、代码实现细节及最终发送效果展示,强调了用户体验的重要性。

在这里插入图片描述

模板消息是指:按照一定的模板样式发送给用户的消息,顾名思义,它的内容必须限制在某一个模板框框内,只能做填空题,做不了主观题。
场景举例:用户A下了订单并交易成功,应该给该用户手机端下发一条交易提醒的通知消息,提升用户体验感。

限制:通知标题和字段内容只能从平台给出的模板里面选择,无法自定义,如果平台提供的模板确实都无法符合业务场景的,可以向平台申请新模板和关键字,不过一提到申请,不用说,按尿性肯定是要审核时间的,还可能一次通过不了,要修改了再提交审核,反正就是要花时间。

不过按照官方提示,目前只有今日头条支持,抖音和 lite 还在接入中。最终效果展示在文末

在这里插入图片描述

1、配置模板消息

第一步就是要先到小程序管理后台去创建消息模板,配置好关键词,生成消息通知ID,这个ID至关重要,因为它是模板消息接口必传参数之一,如下。

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2、代码实现

看一下发送消息的方法,如下:

/**
     * 字节跳动发送模板消息通知
     * https://developer.toutiao.com/docs/server/template_message/send.html
     * @param templateVo
     * @return
     */
    public static boolean sendMessage(TemplateVo templateVo) {
        boolean flag = false;
        TouTiaoTemplate t = new TouTiaoTemplate();
        t.setAccess_token(templateVo.getAccessToken());
        t.setTouser(templateVo.getOpenId());
        t.setTemplate_id(templateVo.getTemplateId());
        t.setPage(templateVo.getPage());
        t.setForm_id(templateVo.getFormId());
        Map<String, SubData> m = new HashMap<String, SubData>();
        List<String> values = templateVo.getValues();
        int nums = values.size();
        for (int i = 0;i < nums;i++) {
            SubData keyword = new SubData();
            keyword.setValue(values.get(i));
            m.put("keyword"+i, keyword);
        }
        t.setData(m);
        String requestParam = JSON.toJSONString(t);
        JSONObject jsonObject = CommonUtil.httpsRequestJson("https://developer.toutiao.com/api/apps/game/template/send",
                "POST", requestParam);
        logger.info("头条消息推送模板json======{}",jsonObject);
        if (jsonObject != null) {
            int errorCode = jsonObject.getInt("errcode");
            String errorMessage = jsonObject.getString("errmsg");
            if (errorCode == 0) {
                logger.info(errorCode + "," + errorMessage);
                flag = true;
            } else {
                logger.info("头条消息发送失败:" + errorCode + "," + errorMessage);
                flag = false;
            }
        }
        return flag;
    }

针对上面的 TemplateVo 参数所包含的字段解释一下:

String openId:要发送给用户的openid,必传

String accessToken:服务端API调用标识,必传

String formId:可以通过组件获得form_id,必传,这个是前端同学传过来的,不用我们后台关心

String templateId:在开发者平台配置消息模版后获得的模版id,必传

String page:点击消息卡片之后打开的小程序页面地址,空则无跳转,非必传,个人觉得传了比较好,也不差这一个参数,让用户点击模板消息就到相应的页面,体验会更好,比如到首页,格式如:pages/index/index

List values:这是一个数组,就是对应于我们模板消息的各个关键字的值,顺序一定要对,不能乱

同样,针对上面的 TouTiaoTemplate 参数所包含的字段解释一下,和TemplateVo差不多,只不过TouTiaoTemplate封装的是直接传给抖音接口的参数,TemplateVo是为了方便外部调用而封装的参数:

access_token:服务端API调用标识,必传

touser:要发送给用户的openid,必传

template_id:在开发者平台配置消息模版后获得的模版id,必传

page:点击消息卡片之后打开的小程序页面地址,空则无跳转,非必传,个人觉得传了比较好,也不差这一个参数,让用户点击模板消息就到相应的页面,体验会更好,比如到首页,格式如:pages/index/index

form_id:可以通过组件获得form_id,必传,这个是前端传过来的,不用我们后台关心

data:模板中填充着的数据,key必须是keyword为前缀,必传,注意,要求是key必须是keyword为前缀,如下面的for循环那样拼接,取到传入的values数组,然后遍历出来拼接上keyword前缀

for (int i = 0;i < nums;i++) {
    SubData keyword = new SubData();
    keyword.setValue(values.get(i));
    m.put("keyword"+i, keyword);
}

httpsRequestJson 这个http请求方法代码参考实现微信扫描二维码关注公众号,直接注册登录网站这篇文章。

3、发送效果

说再多都没用,看下最终的效果哈

在这里插入图片描述
在这里插入图片描述

# 跨平台动画新范式:在uni-app中深度驾驭Lottie-miniprogram 最近在重构几个小程序项目时,我遇到了一个颇为棘手的问题:UI设计师交付的动画效果越来越复杂,传统的GIF和CSS动画已经难以满足需求,尤其是在需要跨多个小程序平台(微信、、支付宝等)保持完全一致的视觉体验时。设计师推荐了Lottie,这个由Airbnb开源的动画解决方案,但当我真正要在uni-app项目中落地时,却发现这条路并不像想象中那么平坦。 市面上关于Lottie在原生微信小程序中的资料还算丰富,但一旦切换到uni-app这个跨端框架,很多看似简单的步骤都会变成需要仔细琢磨的“坑”。特别是`lottie-miniprogram`这个专门为小程序环境优化的版本,其与uni-app的结合使用,文档零散,实践经验更是稀缺。经过几个项目的实战打磨,我总结出了一套相对完整的集成方案,不仅能在微信小程序上完美运行,还能平滑适配字节跳动、百度等主流小程序平台。 这篇文章就是为你——那些需要在多端小程序中实现高质量、高性能动画效果的前端开发者准备的。我会从最基础的原理讲起,一步步带你绕过那些我踩过的坑,最终实现一个稳定、可复用的跨平台动画组件。 ## 1. 理解核心:为什么是Lottie-miniprogram? 在深入代码之前,我们有必要先厘清几个关键概念。Lottie本身是一个库,它能够解析由Adobe After Effects导出的JSON格式的动画数据(通过Bodymovin插件),并在Web、iOS、Android等平台上实时渲染这些动画。其核心优势在于,设计师在AE中制作的复杂动画(包括形状变换、路径动画、蒙版、表达式等)可以无损地转化为代码驱动的动画,彻底解放了前端工程师手动还原动画的苦役。 然而,标准的`lottie-web`库是为浏览器环境设计的,它重度依赖HTML5的`<canvas>`或`<svg>`元素。小程序环境有其特殊性:它虽然也提供了Canvas组件,但其API、运行机制和性能限制都与浏览器有显著差异。直接使用`lottie-web`在小程序中,往往会遇到API不兼容、性能低下甚至直接报错的问题。 > **注意**:`lottie-miniprogram`并非官方出品,而是社区针对小程序环境专门封装的版本。它剥离了浏览器特有的依赖,并适配了小程序的Canvas API和模块系统,是连接Lottie动画与小程序生态的桥梁。 那么,在uni-app中为什么还要大费周章地使用它呢?答案在于**一致性**和**效率**。 * **设计还原度**:UI交付一个JSON文件,你就能获得与设计稿100%一致的动画,无需沟通折损。 * **跨端一致性**:同一份JSON动画数据,配合`lottie-miniprogram`,可以在微信、、支付宝、百度等各家小程序上获得完全相同的视觉效果,避免了为每个平台单独适配动画代码的麻烦。 * **性能与体积**:相比于序列帧或大体积GIF,Lottie动画的JSON数据通常更小,且由CPU/GPU协同渲染,性能更优,尤其适合交互频繁的动画场景。 为了更清晰地对比不同动画方案在小程序端的优劣,我整理了下表: | 特性维度 | Lottie-miniprogram + uni-app | 原生CSS3动画 | 帧动画(GIF/序列图) | 各平台原生动画API | | :--- | :--- | :--- | :--- | :--- | | **设计还原度** | **极高**(AE直接导出) | 中等(需代码实现) | 高(但易失真) | 低(依赖平台能力) | | **跨端一致性** | **极佳**(一份代码,多端运行) | 佳(但需处理样式兼容) | 佳 | 差(各平台API不同) | | **开发效率** | 高(对接设计稿) | 低(手动编写) | 中(需切图) | 低(需多套代码) | | **动画复杂度** | 支持**极高**复杂度 | 支持中等复杂度 | 支持高复杂度(但体积大) | 支持中等复杂度 | | **运行时性能** | 优(Canvas渲染) | 优(GPU加速) | 差(GIF) / 中(序列图) | 优 | | **灵活性** | 中(JSON控制) | **极高**(代码控制) | 低 | 高(代码控制) | 从表格可以看出,对于追求高保真设计还原和跨端一致性的项目,`Lottie-miniprogram`是当前uni-app生态下的最优解之一。 ## 2. 环境搭建与项目初始化 理论清晰后,我们开始动手。首先确保你有一个正在开发中的uni-app项目。如果还没有,可以通过HBuilderX的脚手架快速创建一个。 ### 2.1 安装依赖 `lottie-miniprogram`需要通过npm安装。在项目根目录下打开终端,执行: ```bash npm install lottie-miniprogram --save ``` 或者,如果你使用的是yarn: ```bash yarn add lottie-miniprogram ``` 安装完成后,你需要在**每一个**需要使用Lottie的页面或组件的脚本中引入它。这是与普通Web开发不同的地方,因为小程序每个文件模块是相对独立的。 ```javascript // 在你的页面或组件的 <script> 标签内 import lottie from 'lottie-miniprogram'; ``` ### 2.2 准备动画资源 设计师通常会通过AE的Bodymovin插件导出一个包含`data.json`和一个`images`文件夹的压缩包。这里就是第一个关键点:**小程序环境无法直接加载本地的JSON文件**。 你需要将`data.json`文件改造成一个JavaScript模块。具体操作如下: 1. 将`data.json`重命名为`animationData.js`(名字可自定)。 2. 打开这个js文件,在其内容的最前面加上一句模块导出语句。 原始JSON内容可能是这样的: ```json {"v":"5.7.4","fr":60,"ip":0,"op":180,"w":800,"h":600,"nm":"Comp 1","ddd":0,"assets":[],"layers":[]} ``` 改造后`animationData.js`的内容应该是: ```javascript module.exports = {"v":"5.7.4","fr":60,"ip":0,"op":180,"w":800,"h":600,"nm":"Comp 1","ddd":0,"assets":[],"layers":[]}; ``` 3. 将这个`animationData.js`文件以及配套的`images`文件夹,一起放置到你的uni-app项目的`static`目录下(或其他你指定的静态资源目录)。例如:`/static/lottie/animationData.js`。 ### 2.3 处理图片资源路径(关键坑点) 如果你的动画包含了图片(即JSON中的`assets`数组里有图片对象),那么上面的改造还不够。Bodymovin导出的JSON中,图片路径引用方式在小程序里是行不通的。你需要手动修改`animationData.js`里的图片对象。 找到JSON中`assets`数组里类型为图片的对象,它通常长这样: ```json { "id": "image_0", "w": 300, "h": 200, "u": "images/", "p": "img_0.png", "e": 0 } ``` - `u`: 代表图片的基础路径。 - `p`: 代表图片文件名。 - `e`: 标识图片资源是嵌入的(1)还是外链的(0)。 在小程序中,我们需要使用`require`语法来引入本地图片,并告诉Lottie这个图片是“嵌入”的。因此,需要将其修改为: ```javascript { "id": "image_0", "w": 300, "h": 200, "u": "", // 清空基础路径 "p": require("./images/img_0.png"), // 使用require引入图片,路径相对于当前js文件 "e": 1 // 将e改为1,表示嵌入资源 } ``` > **提示**:这是一个繁琐但必要的过程,尤其是对于包含多张图片的动画。你可以编写一个简单的Node.js脚本来自动化完成这个转换,以提升效率。 ## 3. 核心实现:在Canvas上加载与控制动画 资源准备妥当,接下来就是在页面上渲染动画了。这涉及到uni-app的Canvas组件和`lottie-miniprogram`的API调用。 ### 3.1 构建页面模板 在vue文件的`<template>`部分,我们需要一个Canvas组件作为动画的渲染容器,并添加一些控制按钮。 ```html <view class="lottie-container"> <!-- 关键:canvas的id用于查询,type="2d"启用2d渲染上下文,样式定义宽高 --> <canvas id="lottieCanvas" type="2d" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" canvas-id="lottieCanvasId" ></canvas> <view class="control-buttons"> <button @tap="initLottie" :disabled="inited">初始化动画</button> <button @tap="playAnimation" :disabled="!inited || isPlaying">播放</button> <button @tap="pauseAnimation" :disabled="!inited || !isPlaying">暂停</button> <button @tap="stopAnimation" :disabled="!inited">停止</button> </view> </view> ``` 这里有几个细节: * `id`和`canvas-id`:在小程序原生和uni-app中,有时需要通过`id`来查询节点,通过`canvas-id`来指定画布标识,两者都设置更保险。 * `type="2d"`:**这是必须的**。`lottie-miniprogram`需要Canvas的2D渲染上下文,而不是旧版的WebGL或旧接口。 * 样式宽高:通过样式设置的宽高是画布在页面上的显示尺寸。我们稍后还需要在JS中设置画布的内在像素尺寸,两者最好一致以避免缩放模糊。 ### 3.2 编写动画逻辑 在`<script>`部分,我们将实现动画的初始化、播放、暂停等核心逻辑。这里面的坑点最多。 ```javascript import lottie from 'lottie-miniprogram'; export default { data() { return { canvasWidth: 300, canvasHeight: 300, inited: false, // 动画是否初始化 isPlaying: false, // 动画是否正在播放 animationInstance: null // 保存Lottie动画实例的引用 }; }, // 页面卸载时销毁动画,释放资源 onUnload() { if (this.animationInstance) { this.animationInstance.destroy(); this.animationInstance = null; this.inited = false; } }, methods: { async initLottie() { if (this.inited) { console.warn('动画已经初始化过了'); return; } // 使用uni.createSelectorQuery查询Canvas节点 // 在Vue3的Composition API或某些组件中,可能需要用this.$scope或nextTick确保节点已挂载 const query = uni.createSelectorQuery().in(this); query.select('#lottieCanvas') .fields({ node: true, size: true }) .exec(async (res) => { if (!res[0]) { uni.showToast({ title: '找不到Canvas节点', icon: 'none' }); return; } const canvasNode = res[0].node; const canvas = canvasNode; // 1. 设置Canvas内在分辨率(与显示样式一致,避免模糊) canvas.width = this.canvasWidth; canvas.height = this.canvasHeight; // 2. 获取2D渲染上下文 const ctx = canvas.getContext('2d'); if (!ctx) { uni.showToast({ title: '获取Canvas上下文失败', icon: 'none' }); return; } // 3. 关键步骤:使用lottie.setup注册Canvas lottie.setup(canvas); try { // 4. 动态导入动画数据JS文件 // 注意:require路径相对于当前文件,且静态资源需放在可访问目录 const animationData = require('@/static/lottie/animationData.js'); // 5. 加载动画 this.animationInstance = lottie.loadAnimation({ loop: true, // 是否循环播放 autoplay: false, // 初始化后是否自动播放,这里设为false由按钮控制 animationData: animationData, // 动画数据 rendererSettings: { context: ctx // 传入2D上下文 } }); // 6. 监听动画事件 this.animationInstance.addEventListener('complete', () => { console.log('动画播放完成'); this.isPlaying = false; }); this.animationInstance.addEventListener('loopComplete', () => { console.log('动画循环完成一次'); }); this.inited = true; uni.showToast({ title: '动画初始化成功', icon: 'success' }); } catch (error) { console.error('加载动画数据失败:', error); uni.showToast({ title: '动画数据加载失败', icon: 'none' }); } }); }, playAnimation() { if (this.animationInstance && this.inited) { this.animationInstance.play(); this.isPlaying = true; } }, pauseAnimation() { if (this.animationInstance && this.inited) { this.animationInstance.pause(); this.isPlaying = false; } }, stopAnimation() { if (this.animationInstance && this.inited) { this.animationInstance.stop(); // stop会重置动画到第一帧 this.isPlaying = false; } } } }; ``` **代码解析与避坑指南**: 1. **节点查询**:必须使用`uni.createSelectorQuery().in(this)`来获取当前组件/页面范围内的Canvas节点。直接使用`document.getElementById`或`wx.createSelectorQuery`而不指定范围,在uni-app中很可能返回`null`。 2. **Canvas尺寸**:`canvas.width/height`设置的是画布缓冲区的像素数,与CSS中的宽高不同。两者设置成相同的值,才能保证1:1清晰渲染。 3. **`lottie.setup(canvas)`**:这个调用必须在`loadAnimation`之前执行,它的作用是将Lottie库与你指定的Canvas实例进行绑定。 4. **动画数据引入**:使用`require`引入我们改造好的`animationData.js`。确保路径正确。如果动画数据很大,可以考虑异步加载或分包。 5. **实例存储**:将`loadAnimation`返回的实例保存在组件的`data`或一个响应式引用中(Vue3)。**切勿将其挂在`this`的直接属性上**,在uni-app的运行时环境中,这可能导致非预期的响应式更新或循环引用问题。保存在`data`里是最稳妥的做法。 6. **事件监听**:通过`addEventListener`可以监听动画的生命周期事件,如播放完成、循环完成、进入帧等,这对于需要与动画交互的业务逻辑非常有用。 ## 4. 跨平台兼容性深度处理 让动画在微信开发者工具里跑起来只是第一步,我们的目标是“编写一次,到处运行”。不同小程序平台在Canvas实现、模块系统、文件系统上存在细微差别,需要逐一处理。 ### 4.1 平台特性检测与适配 我们可以在代码开始时进行平台检测,并据此应用不同的策略。 ```javascript // 在工具函数文件或组件开头定义平台常量 const PLATFORM = { WECHAT: 'mp-weixin', BYTEDANCE: 'mp-toutiao', // /头条小程序 ALIPAY: 'mp-alipay', BAIDU: 'mp-baidu' }; const currentPlatform = process.env.VUE_APP_PLATFORM || 'mp-weixin'; // 根据构建环境判断 // 在初始化Canvas时,针对不同平台调整 async initLottie() { // ... 前面的查询代码 ... const canvasNode = res[0].node; // 字节跳动小程序可能需要额外的处理 if (currentPlatform === PLATFORM.BYTEDANCE) { // 有时需要等待Canvas ready,可以加一个延时或使用nextTick await this.$nextTick(); } const canvas = canvasNode; // ... 后续代码 ... } ``` ### 4.2 处理常见的平台报错 * **微信小程序**:相对最稳定,遵循上述代码一般无问题。注意真机调试时,Canvas层级很高,可能会覆盖其他组件,需要使用`cover-view`等特殊组件来覆盖其上层的UI。 * **字节跳动(小程序**: * 可能会在控制台看到“`[CanvasContext] canvasId`找不到”之类的警告,但动画通常能正常播放。这个警告可以尝试通过确保`canvas-id`属性正确设置来抑制。 * 图片资源`require`的路径要格外小心,相对路径基准可能与其他平台不同,使用绝对路径`@/`开头更可靠。 * **支付宝小程序**:对Canvas 2D的支持可能在不同基础库版本上有差异。建议在`package.json`中指定较高的支付宝小程序基础库版本要求。 ```json "mp-alipay": { "usingComponents": {}, "scripts": {}, "componentFramework": "glass-easel", "platformConfig": { "minPlatformVersion": "2.0.0" // 指定一个较高的最低版本 } } ``` * **百度小程序**:早期版本对ES6模块和`require`的支持有些怪异。如果遇到问题,尝试将`animationData.js`改写成使用`module.exports = {}`的CommonJS语法,并确保图片`require`路径正确。 ### 4.3 性能优化建议 跨端兼容不仅要能跑,还要跑得流畅。 - **按需加载动画**:如果页面有多个非首屏动画,不要一次性初始化所有Canvas。可以监听页面滚动,当动画进入视口时再触发`initLottie`。 - **控制动画数量**:同时渲染多个复杂的Lottie动画对性能消耗很大。尽量精简动画元素,或考虑将多个动画合并到同一个JSON文件中。 - **复用Canvas**:在类似列表项动画的场景下,可以考虑复用同一个Canvas实例来渲染不同的动画状态,而不是为每个列表项都创建Canvas。 - **及时销毁**:在页面或组件销毁时(`onUnload`),务必调用动画实例的`destroy()`方法,释放内存和事件监听。 ## 5. 进阶应用与封装复用 基础功能实现后,我们可以考虑将其工程化,封装成易于使用的组件,并探索更高级的交互功能。 ### 5.1 封装成uni-app组件 创建一个名为`uni-lottie.vue`的组件文件,接收动画数据路径、宽高、是否自动播放等作为属性。 ```html <!-- uni-lottie.vue --> <template> <view> <canvas :id="canvasId" type="2d" :style="canvasStyle" :canvas-id="canvasId" ></canvas> </view> </template> <script> import lottie from 'lottie-miniprogram'; export default { name: 'UniLottie', props: { src: { // 动画数据js路径 type: String, required: true }, width: { type: Number, default: 300 }, height: { type: Number, default: 300 }, autoplay: { type: Boolean, default: false }, loop: { type: Boolean, default: true } }, data() { return { canvasId: `lottie_${Date.now()}_${Math.random().toString(36).substr(2)}`, // 生成唯一ID instance: null }; }, computed: { canvasStyle() { return `width: ${this.width}px; height: ${this.height}px;`; } }, mounted() { this.$nextTick(() => { this.initAnimation(); }); }, beforeDestroy() { this.destroyAnimation(); }, methods: { async initAnimation() { // ... 将之前的初始化逻辑移入此处,使用this.src加载动画 ... const animationData = require(`@/${this.src}`); // ... 注意:动态require路径可能需要更复杂的处理,或改为由父组件传入require后的数据 }, destroyAnimation() { if (this.instance) { this.instance.destroy(); this.instance = null; } }, // 暴露控制方法给父组件 play() { /* ... */ }, pause() { /* ... */ }, stop() { /* ... */ }, // 获取当前播放进度或跳转到指定帧 goToAndPlay(value, isFrame = false) { /* ... */ } } }; </script> ``` 这样,在业务页面中,你就可以像使用普通组件一样使用Lottie动画了: ```html <template> <view> <uni-lottie src="static/lottie/success.js" :autoplay="true" @complete="onAnimationComplete" /> </view> </template> ``` ### 5.2 实现动画交互 Lottie动画不仅仅是播放,还可以与之交互。例如,将一个播放进度与用户的滚动位置绑定。 ```javascript // 在组件内部或页面中 bindAnimationToScroll() { // 假设有一个滚动容器 uni.createSelectorQuery() .select('.scroll-view') .boundingClientRect() .exec((res) => { const scrollHeight = res[0].height; // 监听滚动 // ... 计算滚动比例 ... const scrollRatio = scrollTop / (scrollHeight - windowHeight); // 将滚动比例映射到动画的总帧数上 if (this.animationInstance) { const totalFrames = this.animationInstance.totalFrames; const targetFrame = Math.floor(scrollRatio * totalFrames); // 跳转到指定帧并暂停,实现“擦除”效果 this.animationInstance.goToAndStop(targetFrame, true); } }); } ``` ### 5.3 动态替换动画内容 在某些场景下,你可能需要根据状态动态切换动画。`lottie-miniprogram`的动画实例提供了`updateDocumentData`等方法,但更常见的做法是销毁旧实例,用新的动画数据创建新实例。 ```javascript changeAnimation(newDataPath) { // 1. 销毁旧动画 if (this.animationInstance) { this.animationInstance.destroy(); } // 2. 加载新数据 const newAnimationData = require(`@/${newDataPath}`); // 3. 重新初始化(可以复用之前的Canvas和Context) this.animationInstance = lottie.loadAnimation({ // ... 配置,传入新的animationData ... animationData: newAnimationData, }); this.inited = true; } ``` 经过以上五个部分的拆解,从原理认知、环境搭建、核心实现、跨端兼容到进阶封装,你应该已经能够在uni-app项目中游刃有余地使用`lottie-miniprogram`了。这套方案在我负责的多个线上项目中已经稳定运行,成功替代了原有的GIF和CSS动画,在提升视觉效果的同时,也显著降低了多端适配的维护成本。实际开发中,最花时间的部分往往是动画资源的预处理(JSON转JS、图片路径修复),建议将这部分流程脚本化,或者与设计师约定使用更简单的、无外部图片的动画素材,能极大提升开发体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

悟空码字

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值