FastAdmin 实现微信 H5 网页分享(自定义标题、描述、图片)

Php   2025-12-03 16:20   73   0  

技术架构

  • 后端框架:FastAdmin (基于 ThinkPHP)

  • 前端技术:Vue.js + 微信 JS-SDK

  • 微信接口:JS-SDK 分享接口


第一部分:FastAdmin 后端代码编写

1. 创建微信配置控制器

首先,我们需要创建一个专门处理微信 JS-SDK 配置的控制器。

创建文件:application/api/controller/Wechat.php

<?php

namespace app\api\controller;

use app\common\controller\Api;
use think\Cache;
use think\Log;
use think\Request;
use think\Config;

/**
 * 微信JS-SDK配置接口
 */
class Wechat extends Api
{
    protected $noNeedLogin = ['config'];
    protected $noNeedRight = ['config'];

    private $config;

    public function _initialize()
    {
        parent::_initialize();
        $this->config = Config::get('wechat');
    }

    /**
     * 获取微信JS-SDK配置
     */
    public function config()
    {
        $url = $this->request->post('url', '');

        if (empty($url)) {
            $this->error('缺少必要参数: url');
        }

        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            $this->error('无效的URL格式');
        }

        $accessToken = $this->getAccessToken();
        if (!$accessToken) {
            $this->error('获取access_token失败');
        }

        $ticket = $this->getJsapiTicket($accessToken);
        if (!$ticket) {
            $this->error('获取jsapi_ticket失败');
        }

        $nonceStr = $this->generateNonceStr();
        $timestamp = time();
        $signature = $this->generateSignature($ticket, $nonceStr, $timestamp, $url);

        $result = [
            'appId' => $this->config['app_id'],
            'timestamp' => $timestamp,
            'nonceStr' => $nonceStr,
            'signature' => $signature
        ];

        $this->success('获取成功', $result);
    }

    /**
     * 获取access_token
     */
    private function getAccessToken()
    {
        $cacheKey = 'wechat_access_token';
        $accessToken = Cache::get($cacheKey);

        if ($accessToken) {
            Log::info('使用缓存的access_token: ' . $accessToken);
            return $accessToken;
        }

        $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->config['app_id']}&secret={$this->config['app_secret']}";

        $response = $this->httpGet($url);
        if (!$response) {
            Log::error('获取access_token网络请求失败');
            return false;
        }

        $data = json_decode($response, true);
        if (!$data || !isset($data['access_token'])) {
            Log::error('获取access_token失败: ' . $response);
            return false;
        }

        // 缓存access_token(提前5分钟过期)
        Cache::set($cacheKey, $data['access_token'], $data['expires_in'] - 300);

        Log::info('获取新的access_token: ' . $data['access_token']);
        return $data['access_token'];
    }

    /**
     * 获取jsapi_ticket
     */
    private function getJsapiTicket($accessToken)
    {
        $cacheKey = 'wechat_jsapi_ticket';
        $ticket = Cache::get($cacheKey);

        if ($ticket) {
            Log::info('使用缓存的jsapi_ticket: ' . $ticket);
            return $ticket;
        }

        $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={$accessToken}&type=jsapi";

        $response = $this->httpGet($url);
        if (!$response) {
            Log::error('获取jsapi_ticket网络请求失败');
            return false;
        }

        $data = json_decode($response, true);
        if (!$data || !isset($data['ticket'])) {
            Log::error('获取jsapi_ticket失败: ' . $response);
            return false;
        }

        // 缓存jsapi_ticket(提前5分钟过期)
        Cache::set($cacheKey, $data['ticket'], $data['expires_in'] - 300);

        Log::info('获取新的jsapi_ticket: ' . $data['ticket']);
        return $data['ticket'];
    }

    /**
     * 生成签名
     */
    private function generateSignature($ticket, $nonceStr, $timestamp, $url)
    {
        $string = "jsapi_ticket={$ticket}&noncestr={$nonceStr}&timestamp={$timestamp}&url={$url}";
        $signature = sha1($string);

        Log::info('签名字符串: ' . $string);
        Log::info('生成签名: ' . $signature);

        return $signature;
    }

    /**
     * 生成随机字符串
     */
    private function generateNonceStr($length = 16)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $str = '';
        for ($i = 0; $i < $length; $i++) {
            $str .= $chars[mt_rand(0, strlen($chars) - 1)];
        }
        return $str;
    }

    /**
     * HTTP GET请求
     */
    private function httpGet($url)
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_TIMEOUT, 30);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($curl, CURLOPT_URL, $url);

        $response = curl_exec($curl);
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        if ($httpCode !== 200) {
            Log::error("HTTP请求失败,状态码: {$httpCode}");
            return false;
        }

        return $response;
    }
}

2. 创建微信配置文件

创建文件:application/extra/wechat.php

<?php

return [
    // 微信公众号配置
    'app_id' => 'wx12345def',     // 替换为你的微信公众号AppID
    'app_secret' => 'your_app_secret_here', // 替换为你的微信公众号AppSecret
];

3. 配置路由

编辑文件:application/api/route.php

<?php

use think\Route;

// 微信相关接口
Route::group('wechat', function () {
    Route::post('config', 'api/Wechat/config');
});

// 或者直接配置
Route::post('api/wechat/config', 'api/Wechat/config');

4. 配置跨域(如需要)

编辑文件:application/api/behavior/CORS.php

<?php

namespace app\api\behavior;

class CORS
{
    public function appInit(&$params)
    {
        header('Access-Control-Allow-Origin: *');
        header('Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With');
        header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE');
        header('Access-Control-Max-Age: 1728000');

        if (request()->isOptions()) {
            exit();
        }
    }
}

第二部分:H5 前端代码编写

1. 引入微信 JS-SDK

在 HTML 页面中引入微信 JS-SDK:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>微信分享测试</title>
</head>
<body>
    <!-- 页面内容 -->

    <!-- 微信JS-SDK -->
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
    <!-- 你的业务代码 -->
    <script src="./js/wechat-share.js"></script>
</body>
</html>

2. 创建微信分享工具类

创建文件:js/wechat-share.js

/**
 * 微信分享工具类
 */
class WechatShare {
    constructor() {
        this.isConfigured = false;
        this.shareConfig = {};
    }

    /**
     * 检查是否在微信环境
     */
    static isWechat() {
        const ua = navigator.userAgent.toLowerCase();
        return ua.includes('micromessenger');
    }

    /**
     * 获取微信JS-SDK配置
     */
    static async getWechatConfig(url) {
        try {
            const response = await fetch('/api/wechat/config', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ url })
            });

            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }

            const result = await response.json();

            if (result.code !== 1) {
                throw new Error(result.msg || '获取微信配置失败');
            }

            return result.data;
        } catch (error) {
            console.error('获取微信配置失败:', error);
            throw error;
        }
    }

    /**
     * 配置微信JS-SDK
     */
    async config(config) {
        return new Promise((resolve, reject) => {
            wx.config({
                debug: false, // 生产环境设为false
                appId: config.appId,
                timestamp: config.timestamp,
                nonceStr: config.nonceStr,
                signature: config.signature,
                jsApiList: [
                    'updateAppMessageShareData',  // 新版分享到朋友
                    'updateTimelineShareData',    // 新版分享到朋友圈
                    'onMenuShareAppMessage',      // 旧版分享到朋友(兼容)
                    'onMenuShareTimeline'         // 旧版分享到朋友圈(兼容)
                ]
            });

            wx.ready(() => {
                console.log('微信JS-SDK配置成功');
                this.isConfigured = true;
                this.setupShare();
                resolve(true);
            });

            wx.error((res) => {
                console.error('微信JS-SDK配置失败:', res);
                reject(res);
            });
        });
    }

    /**
     * 设置分享配置
     */
    setShareConfig(config) {
        this.shareConfig = {
            title: config.title || document.title,
            desc: config.desc || document.querySelector('meta[name="description"]')?.content || '',
            link: config.link || window.location.href,
            imgUrl: config.imgUrl || this.getDefaultShareImage()
        };

        // 如果已经配置好了,立即设置分享
        if (this.isConfigured) {
            this.setupShare();
        }
    }

    /**
     * 获取默认分享图片
     */
    getDefaultShareImage() {
        // 尝试获取页面中的第一张图片
        const firstImg = document.querySelector('img');
        if (firstImg && firstImg.src) {
            return firstImg.src;
        }

        // 或者返回网站logo
        return `${window.location.origin}/static/images/logo.png`;
    }

    /**
     * 设置分享内容
     */
    setupShare() {
        if (!this.isConfigured || !this.shareConfig.title) {
            return;
        }

        console.log('设置分享内容:', this.shareConfig);

        // 新版微信分享API
        wx.updateAppMessageShareData({
            title: this.shareConfig.title,
            desc: this.shareConfig.desc,
            link: this.shareConfig.link,
            imgUrl: this.shareConfig.imgUrl,
            success: () => {
                console.log('分享到朋友设置成功');
            },
            fail: (res) => {
                console.error('分享到朋友设置失败:', res);
            }
        });

        wx.updateTimelineShareData({
            title: this.shareConfig.title,
            link: this.shareConfig.link,
            imgUrl: this.shareConfig.imgUrl,
            success: () => {
                console.log('分享到朋友圈设置成功');
            },
            fail: (res) => {
                console.error('分享到朋友圈设置失败:', res);
            }
        });

        // 兼容旧版API
        wx.onMenuShareAppMessage({
            title: this.shareConfig.title,
            desc: this.shareConfig.desc,
            link: this.shareConfig.link,
            imgUrl: this.shareConfig.imgUrl,
            success: () => {
                console.log('用户点击分享到朋友');
                this.onShareSuccess('friend');
            },
            cancel: () => {
                console.log('用户取消分享到朋友');
            }
        });

        wx.onMenuShareTimeline({
            title: this.shareConfig.title,
            link: this.shareConfig.link,
            imgUrl: this.shareConfig.imgUrl,
            success: () => {
                console.log('用户点击分享到朋友圈');
                this.onShareSuccess('timeline');
            },
            cancel: () => {
                console.log('用户取消分享到朋友圈');
            }
        });
    }

    /**
     * 分享成功回调
     */
    onShareSuccess(type) {
        // 可以在这里添加分享成功的统计或其他逻辑
        console.log(`分享成功: ${type}`);

        // 发送分享统计到后端
        this.sendShareAnalytics(type);
    }

    /**
     * 发送分享统计
     */
    async sendShareAnalytics(type) {
        try {
            await fetch('/api/analytics/share', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    type: type,
                    title: this.shareConfig.title,
                    url: this.shareConfig.link,
                    timestamp: Date.now()
                })
            });
        } catch (error) {
            console.error('发送分享统计失败:', error);
        }
    }
}

// 导出工具类
window.WechatShare = WechatShare;

3. 在页面中使用微信分享

创建文件:js/app.js

// 页面加载完成后初始化微信分享
document.addEventListener('DOMContentLoaded', async function() {
    // 检查是否在微信环境
    if (!WechatShare.isWechat()) {
        console.log('非微信环境,跳过微信分享配置');
        return;
    }

    // 创建微信分享实例
    const wechatShare = new WechatShare();

    try {
        // 获取当前页面URL(去除hash部分)
        const currentUrl = window.location.href.split('#')[0];

        // 获取微信配置
        console.log('正在获取微信配置...');
        const config = await WechatShare.getWechatConfig(currentUrl);

        // 配置微信JS-SDK
        await wechatShare.config(config);

        // 设置分享内容
        wechatShare.setShareConfig({
            title: '三个桃子·时间里的约定|「Epic Soul」人文阅读体 issue01',
            desc: '每个生命,都有史诗般的灵魂。一颗从时间里走来的果实,在等时间里重逢的人',
            link: 'https://p.test.com/?from=wechat_share',
            imgUrl: 'https://p.test.com/images/share-cover.jpg'
        });

        console.log('微信分享配置完成');

    } catch (error) {
        console.error('微信分享初始化失败:', error);
    }
});

// 动态更新分享内容的函数
function updateShareContent(title, desc, link, imgUrl) {
    if (window.wechatShareInstance) {
        window.wechatShareInstance.setShareConfig({
            title,
            desc,
            link,
            imgUrl
        });
    }
}

4. Vue.js 项目中的使用方式

如果你在 Vue 项目中使用,可以这样封装:

创建文件:src/utils/wechat.js

import WechatShare from './wechat-share.js';

export class WechatUtils {
    constructor() {
        this.wechatShare = null;
        this.isInitialized = false;
    }

    async init() {
        if (this.isInitialized) return;

        if (!WechatShare.isWechat()) {
            console.log('非微信环境');
            return;
        }

        try {
            this.wechatShare = new WechatShare();
            const currentUrl = window.location.href.split('#')[0];
            const config = await WechatShare.getWechatConfig(currentUrl);
            await this.wechatShare.config(config);
            this.isInitialized = true;
            console.log('微信分享初始化成功');
        } catch (error) {
            console.error('微信分享初始化失败:', error);
        }
    }

    setShare(shareConfig) {
        if (this.wechatShare) {
            this.wechatShare.setShareConfig(shareConfig);
        }
    }
}

export default new WechatUtils();


在 Vue 组件中使用:

<template>
  <div class="page">
    <h1>{{ pageTitle }}</h1>
    <p>{{ pageDesc }}</p>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue';
import wechatUtils from '@/utils/wechat';

const pageTitle = ref('文章标题');
const pageDesc = ref('文章描述');

onMounted(async () => {
  // 初始化微信分享
  await wechatUtils.init();

  // 设置分享内容
  wechatUtils.setShare({
    title: pageTitle.value,
    desc: pageDesc.value,
    link: window.location.href,
    imgUrl: 'https://your-domain.com/share-image.jpg'
  });
});
</script>