微信集成解决方案.md 24 KB

微信集成解决方案(PHP版)

一、环境准备

安装微信SDK

# 安装EasyWeChat SDK
composer require w7corp/easywechat:~6.0

二、基础配置

2.1 微信公众号配置

// config/wechat.php
return [
    'official_account' => [
        'app_id' => env('WECHAT_OFFICIAL_ACCOUNT_APPID', ''),
        'secret' => env('WECHAT_OFFICIAL_ACCOUNT_SECRET', ''),
        'token' => env('WECHAT_OFFICIAL_ACCOUNT_TOKEN', ''),
        'aes_key' => env('WECHAT_OFFICIAL_ACCOUNT_AES_KEY', ''),
    ],
    'redis' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0,
    ],
];

2.2 环境变量配置

# .env
WECHAT_OFFICIAL_ACCOUNT_APPID=your_appid
WECHAT_OFFICIAL_ACCOUNT_SECRET=your_secret
WECHAT_OFFICIAL_ACCOUNT_TOKEN=your_token
WECHAT_OFFICIAL_ACCOUNT_AES_KEY=your_aes_key

三、微信通知系统实现

3.1 消息模板定义

// app/Constants/WxTemplateConstants.php
<?php

namespace App\Constants;

class WxTemplateConstants
{
    // 会员用户模板
    const MEMBER_APPOINTMENT_REMINDER = [
        'template_id' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        'title' => '预约提醒',
        'data' => [
            'first' => '您有新的预约提醒',
            'keyword1' => '预约项目',
            'keyword2' => '预约时间',
            'keyword3' => '预约地点',
            'remark' => '请准时到达'
        ]
    ];

    // 技师模板
    const TECHNICIAN_NEW_ORDER = [
        'template_id' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        'title' => '新订单通知',
        'data' => [
            'first' => '您有新的订单',
            'keyword1' => '订单编号',
            'keyword2' => '服务项目',
            'keyword3' => '预约时间',
            'remark' => '请及时确认'
        ]
    ];
}

3.2 通知服务实现

// app/Services/WxNotificationService.php
<?php

namespace App\Services;

use EasyWeChat\Factory;
use App\Constants\WxTemplateConstants;

class WxNotificationService
{
    private $app;

    public function __construct()
    {
        $this->app = Factory::officialAccount(config('wechat.official_account'));
    }

    /**
     * 发送会员预约提醒
     */
    public function sendMemberAppointmentReminder($openId, $data)
    {
        $template = WxTemplateConstants::MEMBER_APPOINTMENT_REMINDER;
        return $this->sendTemplate($openId, $template['template_id'], [
            'first' => ['value' => $template['data']['first']],
            'keyword1' => ['value' => $data['service_name']],
            'keyword2' => ['value' => $data['appointment_time']],
            'keyword3' => ['value' => $data['location']],
            'remark' => ['value' => $template['data']['remark']]
        ]);
    }

    /**
     * 发送技师新订单通知
     */
    public function sendTechnicianNewOrder($openId, $data)
    {
        $template = WxTemplateConstants::TECHNICIAN_NEW_ORDER;
        return $this->sendTemplate($openId, $template['template_id'], [
            'first' => ['value' => $template['data']['first']],
            'keyword1' => ['value' => $data['order_no']],
            'keyword2' => ['value' => $data['service_name']],
            'keyword3' => ['value' => $data['appointment_time']],
            'remark' => ['value' => $template['data']['remark']]
        ]);
    }

    /**
     * 发送模板消息
     */
    private function sendTemplate($openId, $templateId, $data)
    {
        try {
            $result = $this->app->template_message->send([
                'touser' => $openId,
                'template_id' => $templateId,
                'data' => $data
            ]);

            // 记录日志
            \Log::info('WxNotification', [
                'openId' => $openId,
                'templateId' => $templateId,
                'data' => $data,
                'result' => $result
            ]);

            return $result;
        } catch (\Exception $e) {
            \Log::error('WxNotification Error', [
                'message' => $e->getMessage(),
                'openId' => $openId,
                'templateId' => $templateId
            ]);
            throw $e;
        }
    }
}

3.3 队列处理

// app/Jobs/SendWxNotificationJob.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\WxNotificationService;

class SendWxNotificationJob implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    private $openId;
    private $type;
    private $data;

    public function __construct($openId, $type, $data)
    {
        $this->openId = $openId;
        $this->type = $type;
        $this->data = $data;
    }

    public function handle(WxNotificationService $service)
    {
        try {
            switch ($this->type) {
                case 'member_appointment':
                    $service->sendMemberAppointmentReminder($this->openId, $this->data);
                    break;
                case 'technician_order':
                    $service->sendTechnicianNewOrder($this->openId, $this->data);
                    break;
            }
        } catch (\Exception $e) {
            // 失败重试
            if ($this->attempts() < 3) {
                $this->release(30);
            }
        }
    }
}

3.4 使用示例

// 在业务代码中使用
use App\Jobs\SendWxNotificationJob;

// 发送会员预约提醒
dispatch(new SendWxNotificationJob($openId, 'member_appointment', [
    'service_name' => '全身按摩',
    'appointment_time' => '2024-01-20 14:30',
    'location' => 'xxx店'
]));

// 发送技师新订单通知
dispatch(new SendWxNotificationJob($openId, 'technician_order', [
    'order_no' => 'ORDER2024011001',
    'service_name' => '全身按摩',
    'appointment_time' => '2024-01-20 14:30'
]));

四、错误处理与监控

4.1 异常处理

// app/Exceptions/WxNotificationException.php
<?php

namespace App\Exceptions;

class WxNotificationException extends \Exception
{
    protected $data;

    public function __construct($message, $data = [])
    {
        parent::__construct($message);
        $this->data = $data;
    }

    public function getData()
    {
        return $this->data;
    }
}

4.2 监控实现

// app/Services/WxMonitorService.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Redis;

class WxMonitorService
{
    /**
     * 记录发送统计
     */
    public function recordSendStats($type, $success = true)
    {
        $date = date('Y-m-d');
        $key = "wx:stats:{$date}:{$type}";

        Redis::hincrby($key, $success ? 'success' : 'fail', 1);
        Redis::expire($key, 86400 * 7); // 保存7天
    }

    /**
     * 获取发送统计
     */
    public function getSendStats($type, $date = null)
    {
        $date = $date ?: date('Y-m-d');
        $key = "wx:stats:{$date}:{$type}";

        $stats = Redis::hgetall($key);
        $total = array_sum($stats);

        return [
            'total' => $total,
            'success' => (int)($stats['success'] ?? 0),
            'fail' => (int)($stats['fail'] ?? 0),
            'success_rate' => $total > 0 ? round($stats['success'] / $total * 100, 2) : 0
        ];
    }
}

五、配置说明

5.1 微信公众平台配置

  1. 登录微信公众平台
  2. 获取开发者ID(AppID)和开发者密码(AppSecret)
  3. 设置服务器配置
  4. 配置消息模板

5.2 注意事项

  1. 所有配置信息应该放在环境变量中
  2. 定期检查access_token的有效性
  3. 做好日志记录和异常处理
  4. 考虑消息发送的频率限制

六、微信公众号登录系统

6.1 数据库设计

-- 微信用户表
CREATE TABLE `wx_users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `openid` varchar(64) NOT NULL COMMENT '微信openid',
  `unionid` varchar(64) DEFAULT NULL COMMENT '微信unionid',
  `nickname` varchar(50) DEFAULT NULL COMMENT '微信昵称',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  `user_id` bigint(20) DEFAULT NULL COMMENT '关联用户ID',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_openid` (`openid`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';

6.2 前端实现

// resources/js/wechat-auth.js

// 获取微信授权URL
function getWxAuthUrl() {
    const redirectUri = encodeURIComponent(window.location.origin + '/wechat/callback');
    const appId = process.env.MIX_WECHAT_APPID;
    return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
}

// 检查是否在微信浏览器中
function isWechatBrowser() {
    const ua = navigator.userAgent.toLowerCase();
    return ua.indexOf('micromessenger') !== -1;
}

// 微信登录处理
function handleWechatLogin() {
    if (isWechatBrowser()) {
        // 检查是否已有token
        const token = localStorage.getItem('token');
        if (!token) {
            // 跳转到微信授权页面
            window.location.href = getWxAuthUrl();
        }
    }
}

6.3 后端实现

6.3.1 路由配置

// routes/web.php
Route::prefix('wechat')->group(function () {
    Route::get('/auth', 'WechatAuthController@auth');
    Route::get('/callback', 'WechatAuthController@callback');
});

6.3.2 控制器实现

// app/Http/Controllers/WechatAuthController.php
<?php

namespace App\Http\Controllers;

use App\Services\WechatAuthService;
use Illuminate\Http\Request;

class WechatAuthController extends Controller
{
    protected $wechatAuthService;

    public function __construct(WechatAuthService $wechatAuthService)
    {
        $this->wechatAuthService = $wechatAuthService;
    }

    /**
     * 发起微信授权
     */
    public function auth()
    {
        $redirectUrl = url('/wechat/callback');
        return $this->wechatAuthService->getAuthUrl($redirectUrl);
    }

    /**
     * 处理微信回调
     */
    public function callback(Request $request)
    {
        try {
            $code = $request->input('code');
            if (empty($code)) {
                throw new \Exception('授权失败');
            }

            // 处理微信授权
            $result = $this->wechatAuthService->handleCallback($code);

            // 返回token给前端
            return view('wechat.callback', ['token' => $result['token']]);
        } catch (\Exception $e) {
            \Log::error('WechatAuth Error', ['message' => $e->getMessage()]);
            return response()->json(['message' => '授权失败'], 400);
        }
    }
}

6.3.3 服务层实现

// app/Services/WechatAuthService.php
<?php

namespace App\Services;

use App\Models\WxUser;
use App\Models\User;
use EasyWeChat\Factory;
use Illuminate\Support\Str;

class WechatAuthService
{
    protected $app;

    public function __construct()
    {
        $this->app = Factory::officialAccount(config('wechat.official_account'));
    }

    /**
     * 获取授权URL
     */
    public function getAuthUrl($redirectUrl)
    {
        $response = $this->app->oauth->scopes(['snsapi_userinfo'])
            ->redirect($redirectUrl);
        return $response;
    }

    /**
     * 处理微信回调
     */
    public function handleCallback($code)
    {
        try {
            // 获取OAuth用户信息
            $user = $this->app->oauth->user();
            $original = $user->getOriginal();

            // 查找或创建微信用户
            $wxUser = WxUser::firstOrCreate(
                ['openid' => $original['openid']],
                [
                    'unionid' => $original['unionid'] ?? null,
                    'nickname' => $original['nickname'],
                    'avatar' => $original['headimgurl']
                ]
            );

            // 关联或创建系统用户
            $systemUser = $this->findOrCreateSystemUser($wxUser, $original);

            // 生成token
            $token = $this->generateToken($systemUser);

            return [
                'token' => $token,
                'user' => $systemUser
            ];
        } catch (\Exception $e) {
            \Log::error('WechatAuth Callback Error', [
                'code' => $code,
                'message' => $e->getMessage()
            ]);
            throw $e;
        }
    }

    /**
     * 查找或创建系统用户
     */
    private function findOrCreateSystemUser($wxUser, $wxInfo)
    {
        if ($wxUser->user_id) {
            return User::find($wxUser->user_id);
        }

        // 创建新用户
        $user = User::create([
            'name' => $wxInfo['nickname'],
            'avatar' => $wxInfo['headimgurl'],
            'password' => bcrypt(Str::random(16))
        ]);

        // 关联微信用户
        $wxUser->update(['user_id' => $user->id]);

        return $user;
    }

    /**
     * 生成JWT Token
     */
    private function generateToken($user)
    {
        return auth()->login($user);
    }
}

6.3.4 回调页面实现

// resources/views/wechat/callback.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>微信授权回调</title>
</head>
<body>
    <script>
        // 存储token
        localStorage.setItem('token', '{{ $token }}');

        // 关闭当前页面,回到来源页
        if (window.opener) {
            window.opener.postMessage({ type: 'wechat_auth_success', token: '{{ $token }}' }, '*');
            window.close();
        } else {
            window.location.href = '/'; // 或者跳转到指定页面
        }
    </script>
</body>
</html>

6.4 使用说明

6.4.1 前端集成

// 在应用入口检查微信登录状态
document.addEventListener('DOMContentLoaded', () => {
    handleWechatLogin();
});

// 监听登录成功消息
window.addEventListener('message', (event) => {
    if (event.data.type === 'wechat_auth_success') {
        // 存储token
        localStorage.setItem('token', event.data.token);
        // 刷新页面或更新状态
        window.location.reload();
    }
});

6.4.2 接口调用示例

// 在需要微信登录的接口中添加中间件
Route::middleware(['auth:api', 'wechat.auth'])->group(function () {
    // 需要微信登录的路由
});

6.5 安全考虑

  1. Token安全

    • 使用HTTPS传输
    • 设置合理的token过期时间
    • 实现token刷新机制
  2. 数据安全

    • 用户信息加密存储
    • 敏感信息脱敏处理
    • 定期清理过期token
  3. 防护措施

    • 实现防重放攻击
    • 添加请求频率限制
    • 记录异常登录行为

6.6 注意事项

  1. 确保微信公众号已获得网页授权能力
  2. 在微信公众平台正确配置授权回调域名
  3. 处理用户取消授权的情况
  4. 实现用户解绑功能
  5. 考虑多设备登录策略

七、微信公众号支付集成

7.1 数据库设计

-- 支付订单表
CREATE TABLE `wx_payment_orders` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL COMMENT '商户订单号',
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `openid` varchar(64) NOT NULL COMMENT '用户openid',
  `amount` int(11) NOT NULL COMMENT '支付金额(分)',
  `description` varchar(128) NOT NULL COMMENT '商品描述',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '支付状态:0未支付 1支付中 2已支付 3已退款 4已关闭',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `notify_data` text COMMENT '回调原始数据',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信支付订单表';

7.2 配置文件

// config/wechat.php
return [
    // ... 其他配置 ...

    'payment' => [
        'default' => [
            'app_id' => env('WECHAT_PAYMENT_APPID', ''),
            'mch_id' => env('WECHAT_PAYMENT_MCH_ID', ''),
            'key' => env('WECHAT_PAYMENT_KEY', ''),
            'cert_path' => storage_path('app/wechat/cert/apiclient_cert.pem'),
            'key_path' => storage_path('app/wechat/cert/apiclient_key.pem'),
            'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL', ''),
        ],
    ],
];

7.3 支付服务实现

7.3.1 支付服务类

// app/Services/WxPaymentService.php
<?php

namespace App\Services;

use App\Models\WxPaymentOrder;
use EasyWeChat\Factory;
use Illuminate\Support\Str;

class WxPaymentService
{
    protected $payment;

    public function __construct()
    {
        $this->payment = Factory::payment(config('wechat.payment.default'));
    }

    /**
     * 创建支付订单
     */
    public function createOrder($userId, $openid, $amount, $description)
    {
        // 创建商户订单号
        $orderNo = date('YmdHis') . Str::random(6);

        // 创建订单记录
        $order = WxPaymentOrder::create([
            'order_no' => $orderNo,
            'user_id' => $userId,
            'openid' => $openid,
            'amount' => $amount,
            'description' => $description,
            'status' => 0
        ]);

        // 调用微信统一下单
        $result = $this->payment->order->unify([
            'body' => $description,
            'out_trade_no' => $orderNo,
            'total_fee' => $amount,
            'trade_type' => 'JSAPI',
            'openid' => $openid,
        ]);

        if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') {
            // 更新订单状态
            $order->update(['status' => 1]);

            // 生成支付参数
            $params = $this->payment->jssdk->bridgeConfig($result['prepay_id']);

            return [
                'order_no' => $orderNo,
                'params' => $params
            ];
        }

        throw new \Exception($result['err_code_des'] ?? '创建支付订单失败');
    }

    /**
     * 处理支付回调
     */
    public function handlePaymentNotify($message)
    {
        // 查找订单
        $order = WxPaymentOrder::where('order_no', $message['out_trade_no'])->first();
        if (!$order) {
            return false;
        }

        // 验证支付金额
        if ($message['total_fee'] != $order->amount) {
            return false;
        }

        // 更新订单状态
        $order->update([
            'status' => 2,
            'transaction_id' => $message['transaction_id'],
            'pay_time' => date('Y-m-d H:i:s'),
            'notify_data' => json_encode($message)
        ]);

        // 触发支付成功事件
        event(new PaymentSuccessEvent($order));

        return true;
    }

    /**
     * 查询订单状态
     */
    public function queryOrder($orderNo)
    {
        $result = $this->payment->order->queryByOutTradeNumber($orderNo);

        if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') {
            return [
                'paid' => $result['trade_state'] === 'SUCCESS',
                'transaction_id' => $result['transaction_id'] ?? null,
                'trade_state' => $result['trade_state'],
                'trade_state_desc' => $result['trade_state_desc']
            ];
        }

        return false;
    }

    /**
     * 申请退款
     */
    public function refund($orderNo, $refundAmount, $reason = '')
    {
        $order = WxPaymentOrder::where('order_no', $orderNo)->first();
        if (!$order || $order->status != 2) {
            throw new \Exception('订单状态不正确');
        }

        $result = $this->payment->refund->byOutTradeNumber($orderNo, date('YmdHis') . Str::random(6), $order->amount, $refundAmount, [
            'refund_desc' => $reason
        ]);

        if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') {
            // 更新订单状态
            $order->update(['status' => 3]);
            return true;
        }

        throw new \Exception($result['err_code_des'] ?? '申请退款失败');
    }
}

7.4 控制器实现

// app/Http/Controllers/WxPaymentController.php
<?php

namespace App\Http\Controllers;

use App\Services\WxPaymentService;
use Illuminate\Http\Request;

class WxPaymentController extends Controller
{
    protected $paymentService;

    public function __construct(WxPaymentService $paymentService)
    {
        $this->paymentService = $paymentService;
    }

    /**
     * 创建支付订单
     */
    public function createOrder(Request $request)
    {
        $this->validate($request, [
            'amount' => 'required|integer|min:1',
            'description' => 'required|string|max:127'
        ]);

        try {
            $result = $this->paymentService->createOrder(
                auth()->id(),
                $request->user()->wx_user->openid,
                $request->input('amount'),
                $request->input('description')
            );

            return response()->json($result);
        } catch (\Exception $e) {
            return response()->json(['message' => $e->getMessage()], 400);
        }
    }

    /**
     * 支付回调
     */
    public function notify()
    {
        $response = $this->paymentService->payment->handlePaidNotify(function($message, $fail) {
            $result = $this->paymentService->handlePaymentNotify($message);
            return $result ? true : $fail('处理失败');
        });

        return $response;
    }

    /**
     * 查询订单
     */
    public function query(Request $request)
    {
        $orderNo = $request->input('order_no');
        $result = $this->paymentService->queryOrder($orderNo);

        return response()->json($result ?: ['message' => '查询失败'], $result ? 200 : 400);
    }
}

7.5 路由配置

// routes/api.php
Route::prefix('payment')->middleware(['auth:api'])->group(function () {
    Route::post('order', 'WxPaymentController@createOrder');
    Route::get('query', 'WxPaymentController@query');
});

// routes/web.php
Route::any('payment/notify', 'WxPaymentController@notify');

7.6 前端调用示例

// 发起支付
async function wxPay(amount, description) {
    try {
        // 1. 创建订单
        const response = await axios.post('/api/payment/order', {
            amount,
            description
        });

        // 2. 调起支付
        const params = response.data.params;
        WeixinJSBridge.invoke('getBrandWCPayRequest', params, function(res) {
            if (res.err_msg === 'get_brand_wcpay_request:ok') {
                // 支付成功
                alert('支付成功');
            } else {
                // 支付失败
                alert('支付失败:' + res.err_msg);
            }
        });
    } catch (error) {
        console.error('支付失败', error);
        alert('创建订单失败');
    }
}

// 查询订单状态
async function queryOrder(orderNo) {
    try {
        const response = await axios.get('/api/payment/query', {
            params: { order_no: orderNo }
        });
        return response.data;
    } catch (error) {
        console.error('查询订单失败', error);
        return null;
    }
}

7.7 安全考虑

  1. 支付安全

    • 必须使用HTTPS
    • 验证签名
    • 校验订单金额
    • 防止重复支付
    • 敏感信息加密存储
  2. 证书管理

    • 安全存储证书文件
    • 定期更新证书
    • 限制证书文件访问权限
  3. 异常处理

    • 处理网络超时
    • 处理重复通知
    • 记录详细日志

7.8 注意事项

  1. 确保微信支付商户号已开通
  2. 正确配置支付回调域名
  3. 处理订单超时
  4. 实现订单关闭功能
  5. 考虑并发支付情况
  6. 实现退款功能
  7. 做好金额计算(避免浮点数精度问题)
  8. 定期对账