# 安装EasyWeChat SDK
composer require w7corp/easywechat:~6.0
// 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,
],
];
# .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
// 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' => '请及时确认'
]
];
}
// 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;
}
}
}
// 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);
}
}
}
}
// 在业务代码中使用
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'
]));
// 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;
}
}
// 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
];
}
}
-- 微信用户表
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='微信用户表';
// 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();
}
}
}
// routes/web.php
Route::prefix('wechat')->group(function () {
Route::get('/auth', 'WechatAuthController@auth');
Route::get('/callback', 'WechatAuthController@callback');
});
// 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);
}
}
}
// 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);
}
}
// 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>
// 在应用入口检查微信登录状态
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();
}
});
// 在需要微信登录的接口中添加中间件
Route::middleware(['auth:api', 'wechat.auth'])->group(function () {
// 需要微信登录的路由
});
Token安全
数据安全
防护措施
-- 支付订单表
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='微信支付订单表';
// 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', ''),
],
],
];
// 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'] ?? '申请退款失败');
}
}
// 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);
}
}
// 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');
// 发起支付
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;
}
}
支付安全
证书管理
异常处理