【微信公众号】微信集成功能--扫描二维码完成用户登录操作

21 篇文章 0 订阅
6 篇文章 0 订阅

目录

需求来源

实现思路

1、进入登录页面,生成微信公众号的临时二维码;

2、用户通过微信扫一扫二维码;

3、登录页面定时查询扫码结果;

代码实现(基于Laravel框架前后端混合)

HTML

PHP-路由文件

PHP-控制器

PHP-模型

数据表(请根据实际业务定义表结构)

感谢阅读,欢迎交流


需求来源

业务系统的PC端增加微信二维码扫码登录功能

实现思路

1、进入登录页面,生成微信公众号的临时二维码;

传入二维码的场景值(开发者接收微信服务器推送的数据包用以区分业务场景)、过期时间(用户无操作时多久刷新一次二维码)

2、用户通过微信扫一扫二维码;

用户扫描二维码,微信服务器推送扫描事件的xml数据包给开发者服务器,开发者通过xml数据包处理用户的扫码登录逻辑,更新用户成功登录的标识;

3、登录页面定时查询扫码结果;

定义轮询方法,通过生成二维码的场景值定时查询用户扫码的结果,成功则处理登录成功的逻辑,反之继续轮询,本文自动更新二维码(可不做自动刷新二维码,二维码到期时停止轮询);

代码实现(基于Laravel框架前后端混合)

HTML

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="{{asset('vendor/jquery-v2.2.4/jquery.min.js')}}"></script>
    <title>{{$title}}</title>
    <style>
        body,html,#container {
            width: 100%;
            height: 100%;
            margin: 0;
        }
        #container {
            margin: 5% auto;
            display: flex;
            align-items: center;
            flex-direction: column;
        }
    </style>
    <link rel="stylesheet" href="{{asset('vendor/lib/layui/css/layui.css')}}">
</head>
<body>
<div id="container">
    @include('plugins.izi-modal')
    @include('plugins.loading')

    @if(!\Session::get('user'))
        <p>打开微信扫一扫</p>
        <div style="width: 200px;height:200px;">
            <img id="qrcode" src="" alt="" style="width:100%;height:100%;">
        </div>
    @else
        <p>用户已登录</p>
        <p><button onclick="wxLogout()">退出登录</button></p>
    @endif
</div>
</body>
</html>
<script src="{{asset('vendor/lib/layui/layui.js')}}"></script>
<script src="{{asset('vendor/system-js/tool.js')}}"></script>
<script type="text/javascript">
    layui.use(['layer'], function () {
      let layer = layui.layer
      let scene = "",expireTime = 0;
      /* todo 创建二维码 */
      createQrcode()
      function createQrcode()
      {
        tool.getData("{{url('wechat/func/login/create_login_qrcode')}}", {}, (res) => {
          if (res.code == 200) {
            $('#qrcode').attr('src', res.data.url)
            scene = res.data.scene
            expireTime = res.data.expireTime
          }
        })
      }

      /* todo 轮询调用接口查询用户是否已扫码完成 */
      checkLoginStatus()
      function checkLoginStatus()
      {
        let timeout = setTimeout(checkLoginStatus, 5*1000)
        if (!scene || expireTime === 0) {return false}
        // 二维码过期重新获取
        if (expireTime < tool.get_timestamp()) {
          createQrcode()
          return false
        }
        let data = {eventKey: scene,expireTime: expireTime}
        tool.getData("{{url('wechat/func/login/check_login_status')}}", data, (res) => {
          // 设置的二维码已过期,重新请求创建
          if (res.code == 201) {
            createQrcode()
          }
          // 用户已扫码登录成功,关闭延时器
          if (res.code == 202) {
            clearTimeout(timeout)
          }
          // 查询扫码登录状态,成功则自刷新页面,根据实际业务处理后续
          if (res.code == 200 && res.data.result == 1) {
            alert("扫码登录成功")
            window.location.reload()
          }
        })
      }
    })

    /* todo 退出微信扫码登录状态 */
    function wxLogout() {
      if (confirm("请确定是否继续退出登录?")) {
        tool.getData("{{url('wechat/func/login/wx_logout')}}", {}, (res) => {
          if (res.code == 200) {
            window.location.reload()
          }
        })
      }
    }
</script>

PHP-路由文件

<?php
use Illuminate\Http\Request;
use Illuminate\Routing\Router;
Route::group(['prefix' => 'wechat/'], function (Router $router) {
    // 接收微信推送信息,完成验证消息真实性
    $router->get('receive_push', "WechatController@receivePush");
    // 接收微信推送信息,完成接收普通消息并回复
    $router->post('receive_push', "WechatController@receivePush");
    // 对话服务-基础支持-获取access_token
    $router->get('get_access_token', 'WechatController@getAccessToken');
    // 微信公众号集成功能
    $router->group(['prefix' => '/func/'], function(Router $router) {
        // 微信公众号集成功能--用户扫码登录
        $router->group(['prefix' => 'login/'], function(Router $router) {
            // 微信公众号集成功能--用户扫码登录--微信登录页面
            $router->get('wx_login_view', 'WechatController@wxLoginView');
            // 微信公众号集成功能--用户扫码登录--创建登录二维码
            $router->get('create_login_qrcode', 'WechatController@createLoginQrcode');
            // 微信公众号集成功能--用户扫码登录--查询扫描二维码状态
            $router->get('check_login_status', 'WechatController@checkLoginStatus');
            // 微信公众号集成功能--用户扫码登录--微信退出登录
            $router->get('wx_logout', 'WechatController@wxLogout');
        });
    });
});

PHP-控制器

<?php
namespace App\Http\Controllers\Wechat;

use App\Enums\WechatEnum;
use App\Models\WechatUser;
use App\Tools\Helper;
use Illuminate\Http\Request;

class WechatController extends BaseController
{
    // todo 接收微信推送--明文模式
    public function receivePush(Request $request)
    {
        file_log("进入访问" . $request->getUri(), "wechat");
        try {
            switch ($_SERVER["REQUEST_METHOD"]) {
                case "GET":
                    file_log("验证消息真实性" . $request->getUri(), "wechat");
                    $params = $request->all();
                    $params['token'] = config("wechat_develop.Token");
                    // 校验签名是否正确
                    $isMatch = $this->verifySignature($params);
                    if (!$isMatch) {
                        exit(json_encode(["errcode" => -40001, "errmsg" => "signature sha1 error"], JSON_UNESCAPED_UNICODE));
                    }
                    echo $params["echostr"];
                    exit;
                    break;
                case "POST":
                    file_log("接收公众号发来的信息" . $request->getUri(), "wechat");
                    // 接收微信公众号传送的xml信息包
                    $this->postXml = isset($GLOBALS["HTTP_RAW_POST_DATA"]) ? $GLOBALS["HTTP_RAW_POST_DATA"] : file_get_contents("php://input");
                    file_log("公众号发来的xml数据包" . $this->postXml, "wechat");
                    // xml转换成array
                    $postArray = json_decode(json_encode(simplexml_load_string($this->postXml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
                    // 按照实际业务处理不同类型的信息,并返回xml包
                    $this->receive = $postArray;
                    @ob_clean();
                    echo $this->reply();
                    exit;
                    break;
                default:
                    file_log("【非验证微信服务器信息】或【非微信服务器发送的信息】" . $request->getUri(), "wechat");
                    exit;
            }
        } catch (\Exception $e) {
            exception_file_log($e, 'wechat');
            exit($e->getMessage() . $e->getTraceAsString());
        }
    }

    private function verifySignature($getParam)
    {
        $tmpArr = array($getParam["token"], $getParam["timestamp"], $getParam["nonce"]);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);
        return $tmpStr == $getParam['signature'];
    }

    private function reply()
    {
        $this->message = [
            "MsgType" => $this->receive["MsgType"],
            "CreateTime" => time(),
            "ToUserName" => $this->receive["FromUserName"],
            "FromUserName" => $this->receive["ToUserName"],
        ];
        switch ($this->receive["MsgType"]) {
            case "event":
                $this->event();
                break;
            default:
                return $this->postXml;
                break;
        }
        $replyXml = self::arr2xml($this->message);
        file_log("开发者发送的信息:" . $replyXml, "wechat");
        return $replyXml;
    }

    public static function arr2xml($data)
    {
        return "<xml>" . self::_arr2xml($data) . "</xml>";
    }

    private static function _arr2xml($data, $xmlContent = '')
    {
        foreach ($data as $key => $val) {
            is_numeric($key) && $key = 'item';
            $xmlContent .= "<{$key}>";
            if (is_array($val) || is_object($val)) {
                $xmlContent .= self::_arr2xml($val);
            } elseif (is_string($val)) {
                $xmlContent .= '<![CDATA[' . preg_replace("/[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]/", '', $val) . ']]>';
            } else {
                $xmlContent .= $val;
            }
            $xmlContent .= "</{$key}>";
        }
        return $xmlContent;
    } 

    # event类型
    private function event()
    {
        $this->message["MsgType"] = "text";
        $this->message["Content"] = "遇到未知推送事件";
        file_log("推送事件名称:" . strtolower($this->receive["Event"]), "wechat");
        switch (strtolower($this->receive["Event"])) {
            case "subscribe":
                $this->message["MsgType"] = "text";
                $this->message["Content"] = "关注公众号触发,暂无做其他处理";
                $this->subscribe();
                break;
            case "scan":
                $this->message["MsgType"] = "text";
                $this->message["Content"] = "扫码二维码触发,二维码附带的场景值:{$this->receive["EventKey"]},Ticket:{$this->receive["Ticket"]}";
                $this->scan();
                break;
            default:
                break;
        }
        WechatPushRecord::create($this->message);
    }

    # 关注公众号逻辑
    private function subscribe()
    {
        // 判断数据库是否已存在用户--实际业务
        if (!$user = WechatUser::get_one(["where" => [["openid", '=', $this->message['ToUserName']]]])) {
            // 拉取用户信息并存库
            $userInfo = $this->user_info(['openid' => $this->message['ToUserName'], 'lang' => 'zh_CN']);
            file_log(json_encode($userInfo,JSON_UNESCAPED_UNICODE),'wechat');
            check_result_issuccess($user = WechatUser::saveSubscribeUser($userInfo), "{$this->message['ToUserName']}保存微信用户信息失败", false, WechatEnum::Exception);
        } else {
            // 更新用户信息--暂时只在保存时提交数据,不做更新用户信息操作
        }
        // 判断是否存在事件键值,格式:qrscene_login-xxxxxxxx
        if (isset($this->receive['EventKey']) && $this->receive['EventKey']) {
            $eventKey = explode('_', $this->receive['EventKey']);
            $scene = $eventKey[0];
            switch ($scene) {
                case 'qrscene':
                    // 扫码登录
                    if (isset($this->receive['Ticket']) && $this->receive['Ticket']) {
                        // 更新当前用户的扫码登录事件键值,表示用户已扫码登录成功
                        WechatUser::updateLoginStatus($this->message['ToUserName'], $eventKey[1]);
                        $this->message['Content'] = "用户扫码登录成功";
                    }
                    break;
            }
        }
    }

    # 扫描二维码逻辑
    private function scan()
    {
        // 定制二维码的场景值时,通过符号“-”进行分割,如login-123456,login为二维码的分类场景值
        if (isset($this->receive['EventKey']) && $this->receive['EventKey']) {
            $scene = explode('-', $this->receive['EventKey'])[0];
            switch ($scene) {
                case 'login': // 扫码登录
                    // 更新当前用户的扫码登录事件键值,表示用户已扫码登录成功
                    WechatUser::updateLoginStatus($this->message['ToUserName'],$this->receive['EventKey']);
                    $this->message['Content'] = "用户扫码登录成功";
                    break;
                default:
                    $this->message['Content'] = "当前二维码场景未定义,请尽快接入";
                    break;
            }
        }
    }

    /**
     * todo 微信公众号集成功能--用户扫码登录--微信登录页面
     */
    public function wxLoginView()
    {
        $user = \Session::get('user');
        $title = "用户扫码登录";
        return view("wechat.wx-login.wxLoginView", compact("title", "user"));
    }

    /**
     * todo 微信公众号集成功能--用户扫码登录--创建登录二维码
     */
    public function createLoginQrcode(Request $request)
    {
        try {
            $eventKey = "login-". uniqid('');
            $expireSeconds = 60 * 20;
            $ticketAndUrl = $this->qrcode_create([
                "expire_seconds" => $expireSeconds,
                "scene" => $eventKey // 可选择:字符串、整型
            ]);
            if (isset($ticketAndUrl['ticket'])) {
                $qrcodeUrl = $this->show_qrcode($ticketAndUrl);
            }
        } catch (\Exception $e) {
            exception_file_log($e, "wechat");
            exit("【createLoginQrcode】创建登录二维码失败,请到wechat日志文件查看详情");
        }
        return return_info(200, '获取二维码链接成功', ['url' => $qrcodeUrl, 'scene' => $eventKey, 'expireTime' => time() + $expireSeconds]);
    }

    /**
     * todo 微信公众号集成功能--用户扫码登录--查询扫描二维码状态
     */
    public function checkLoginStatus(Request $request)
    {
        if (\Session::get('user')) return return_info(202, '用户已登录');
        try {
            if (is_null($eventKey = $request->eventKey))  return return_info(500, '请先传入必要参数-eventKey');
            if (is_null($expireTime = $request->expireTime))  return return_info(500, '请先传入必要参数-expireTime');
            if ($expireTime < time()) return return_info(201, '二维码已过期');
            // 查询扫码登录的事件键值是否已更新到用户的信息中
            $checkLoginStatus = WechatUser::checkLoginStatus($eventKey);
            // 处理扫码成功的逻辑
            if ($checkLoginStatus) {
                # 代码...
                \Session::put('user', $eventKey);
                \Session::save();
            }
        } catch (\Exception $e) {
            exception_file_log($e, 'wechat');
            exit("【checkLoginStatus】查询扫描二维码状态失败,请到wechat日志文件查看详情");
        }
        return return_info(200, '查询扫描状态成功', ['result' => $checkLoginStatus ? 1 : 0]);
    }

    /**
     * todo 微信公众号集成功能--用户扫码登录--微信退出登录
     */
    public function wxLogout(Request $request)
    {
        try {
            if (\Session::get('user')) {
                \Session::put('user', null);
                \Session::save();
            }
        } catch (\Exception $e) {
            exception_file_log($e, 'wechat');
            exit("【wxLogout】微信退出登录失败,请到wechat日志文件查看详情");
        }
        return return_info(200, '微信退出登录成功');
    }

    /**
     * todo 创建二维码ticket
     */
    public function qrcode_create($data)
    {
        $url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$this->getAccessToken()}";
        // 处理action_info
        if (is_integer($data["scene"])) {
            $data["action_info"]["scene"] = ["scene_id" => $data["scene"]];
        } else {
            $data["action_info"]["scene"] = ["scene_str" => $data["scene"]];
        }
        // 处理action_name
        if (isset($data["expire_seconds"]) && $data["expire_seconds"] > 0) {
            $data["action_name"] = is_integer($data["scene"]) ? "QR_SCENE" : "QR_STR_SCENE";
        } else {
            $data["action_name"] = is_integer($data["scene"]) ? "QR_LIMIT_SCENE" : "QR_LIMIT_STR_SCENE";
        }
        return $this->post_http_request($url, $data);
    }

    /**
     * todo 通过ticket换取二维码
     */
    public function show_qrcode($data)
    {
        $ticket = urlencode($data["ticket"]);
        return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
    }

    public function post_http_request($url, $postData)
    {
        $resultArr = json_decode($this->http_url($url, json_encode($postData, JSON_UNESCAPED_UNICODE)), true);
        if (isset($resultArr["errcode"]) && $resultArr["errcode"] != 0) {
            file_log($resultArr, $this->logFile ?: "HttpRequest");
            throw new \Exception($resultArr['errmsg']);
        }
        return $resultArr;
    }

    public function http_url($url, $data = null)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
        $res = curl_exec($ch);
        if (curl_errno($ch)) {
            throw new \Exception("error:" . curl_error($ch));
        }
        curl_close($ch);
        return $res;
    }
}

PHP-模型

<?php
namespace App\Models;
class WechatUser extends Model
{
    protected $table = 'wechat_users';

    const Unknown = 0; // todo 未知
    const Male = 1; // todo 男性
    const Female = 2; // todo 女性

    /**
     * todo 保存微信用户
     */
    public static function saveUser($userInfo)
    {
        if (isset($userInfo["privilege"])){
            unset($userInfo["privilege"]);
        }
        $user = self::insert(new self(),$userInfo);
        return $user ? $user->toArray() : false;
    }

    /**
     * todo 保存关注的用户数据
     */
    public static function saveSubscribeUser($userInfo)
    {
        $insertInfo = [];
        $allowFiled = ["subscribe", "openid", "nickname", "sex", "language", "city", "province", "country", "headimgurl", "subscribe_time", "remark", "groupid", "tagid_list", "subscribe_scene", "qr_scene", "qr_scene_str"];
        foreach ($allowFiled as $field) {
            if (isset($userInfo[$field])) {
                if ($field == "tagid_list") {
                    $userInfo[$field] = json_encode($userInfo[$field],JSON_UNESCAPED_UNICODE);
                }
                $insertInfo[$field] = $userInfo[$field];
            }
        } unset($field);
        $user = self::insert(new self(), $insertInfo);
        return $user ? $user->toArray() : false;
    }

    /**
     * todo 查询用户的扫码登录状态信息
     */
    public static function checkLoginStatus($eventKey,$field = null)
    {
        $param = [];
        $param['where'] = [
            ['wechat_scan_event_key', '=', $eventKey],
            ['wechat_scan_event_key_expire_time', '>', time()]
        ];
        if ($field) $param['field'] = $field;
        return self::get_one($param);
    }

    /**
     * todo 更新用户的扫码登录事件键值
     */
    public static function updateLoginStatus($openId, $eventKey, $expireTime = 3600)
    {
        $where = [['openid', '=', $openId]];
        $data = ['wechat_scan_event_key_expire_time' => time() + $expireTime, 'wechat_scan_event_key' => $eventKey];
        return self::update_by_where(['where' => $where, 'data' => $data]);
    }
}

数据表(请根据实际业务定义表结构)

CREATE TABLE `wechat_users` (
  `ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `openid` varchar(32) DEFAULT '' COMMENT '公众号唯一标识',
  `subscribe` varchar(1) DEFAULT '' COMMENT '是否关注 1-是',
  `nickname` varchar(50) DEFAULT '' COMMENT '关注用户昵称',
  `sex` tinyint(1) unsigned DEFAULT '0' COMMENT '关注用户性别 0:未知 1:男 2:女',
  `province` varchar(50) DEFAULT '' COMMENT '关注用户省份',
  `city` varchar(50) DEFAULT '' COMMENT '关注用户城市',
  `country` varchar(50) DEFAULT '' COMMENT '关注用户国家',
  `headimgurl` varchar(255) DEFAULT '' COMMENT '关注用户头像',
  `unionid` varchar(32) DEFAULT '' COMMENT '关注用户开放平台唯一标识',
  `language` varchar(20) DEFAULT '' COMMENT '关注用户使用语言',
  `subscribe_time` int(10) unsigned DEFAULT '0' COMMENT '关注时间',
  `remark` varchar(100) DEFAULT '' COMMENT '公众号运营者对粉丝的备注',
  `groupid` varchar(10) DEFAULT '' COMMENT '用户所在的分组ID',
  `tagid_list` varchar(255) DEFAULT '' COMMENT '用户被打上的标签ID列表',
  `subscribe_scene` varchar(20) DEFAULT '' COMMENT '用户关注的渠道来源',
  `qr_scene` varchar(20) DEFAULT '' COMMENT '二维码扫码场景(开发者自定义)',
  `qr_scene_str` varchar(50) DEFAULT '' COMMENT '二维码扫码场景描述(开发者自定义)',
  `wechat_scan_event_key` varchar(50) DEFAULT '' COMMENT '用户扫码登录的事件键值',
  `wechat_scan_event_key_expire_time` int(10) unsigned DEFAULT '0' COMMENT '用户扫码登录的事件键值过期时间',
  `created_at` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_at` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='微信公众号用户表';

感谢阅读,欢迎交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
课程概述: 本课程是一个小型的vue周边技术+以php为基础的+微信接口开发的小型项目。本项目以微信扫码关注公众号实现网站自动登陆这一功能为载体,将会讲解如下主要核心知识点:前端你将学习到: 一、vue-cli4进行前端项目的创建 二、如何使用vuex进行登陆信息的管理与同步 三、如何使用axios进行接口请求的封装与拦截 四、在脚手架里使用Element-UI 五、学习组件化编程的思想 六、如何解决接口前后端分离引起的跨域问题以及在跨域下使用cookie凭证进行会话维护 后端你讲学习到: 一、如何申请微信公众号测试账号,如何进行相关参数的配置,如何进行微信相关接口的开发 二、如何使用微信接口开发,如获取临时二维码,获取用户基础信息,监听公众号关注以及扫描事件 三、如何使用redis对数据进行缓存 四、如何使用php原生代码进行接口的开发 五、如何使用laralvel 7.x 框架进行接口的开发 六、学习到laravel 中核心概念的使用方法,如什么是依赖注入,如何使用服务容器解决依赖注入、服务提供者、中间件的使用,如何处理请求数据,如何进行用户认证,以及如何使用Eloquent ORM进行数据库操作 七、开发过程中遇到的问题,如何进行排查 八、git远端仓库的建立与配置,如何在配置多仓库下的公钥,服务器端部署公钥的配置,如何进行代码部署,本地与服务器代码的开发实时同步 九、如何使用composer帮助我们进行第三方依赖包的安装 本课程的设计思路随笔: 从业务层面上来讲,扫码关注公众号,实现网站端自动登陆是一个非常实用的功能,可以为微信公众号引流。 技术层面上来说,使用前后端分离进行制作,可以将前端以及后端的知识都涵盖到。对于前端的路由,信息维护,脚手架的搭建,ui组件的使用,接口的请求与封装都能够讲解到。 对于后端,本课程对php原生代码以及工作中使用频率比较高的同时也很优雅的laravel框架都会进行讲解,分别予以代码的实现。让同学们能够看到原生开发与框架开发的区别,原生开发使得基础比较弱的同学能够慢慢上手,也知道此功能实现的核心要点,在进行原生代码开发后,再进行框架开发,会有个循序渐进的过程,同时在框架里面我们会降到主流框架都会用到的一些核心思想,比如依赖注入,服务容器,中间件等等,同时对于想学习laravel框架的同学,学习过这个案例后,再去看文档就知道该如何去看,如何去学了。 对于整个代码的管理与部署,我们也会引入git仓库进行项目代码管理,如何在服务器进行网站环境的搭建与代码部署等等实用技巧。 相信本课程会给大家带来十足的收获,大家加油。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

东小记

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

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

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

打赏作者

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

抵扣说明:

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

余额充值