|
@@ -0,0 +1,943 @@
|
|
|
+# 微信集成解决方案(PHP版)
|
|
|
+
|
|
|
+## 一、环境准备
|
|
|
+
|
|
|
+### 安装微信SDK
|
|
|
+```bash
|
|
|
+# 安装EasyWeChat SDK
|
|
|
+composer require w7corp/easywechat:~6.0
|
|
|
+```
|
|
|
+
|
|
|
+## 二、基础配置
|
|
|
+
|
|
|
+### 2.1 微信公众号配置
|
|
|
+```php
|
|
|
+// 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
|
|
|
+# .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 消息模板定义
|
|
|
+```php
|
|
|
+// 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 通知服务实现
|
|
|
+```php
|
|
|
+// 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 队列处理
|
|
|
+```php
|
|
|
+// 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 使用示例
|
|
|
+```php
|
|
|
+// 在业务代码中使用
|
|
|
+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 异常处理
|
|
|
+```php
|
|
|
+// 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 监控实现
|
|
|
+```php
|
|
|
+// 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 数据库设计
|
|
|
+```sql
|
|
|
+-- 微信用户表
|
|
|
+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 前端实现
|
|
|
+```javascript
|
|
|
+// 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 路由配置
|
|
|
+```php
|
|
|
+// routes/web.php
|
|
|
+Route::prefix('wechat')->group(function () {
|
|
|
+ Route::get('/auth', 'WechatAuthController@auth');
|
|
|
+ Route::get('/callback', 'WechatAuthController@callback');
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.3.2 控制器实现
|
|
|
+```php
|
|
|
+// 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 服务层实现
|
|
|
+```php
|
|
|
+// 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 回调页面实现
|
|
|
+```php
|
|
|
+// 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 前端集成
|
|
|
+```javascript
|
|
|
+// 在应用入口检查微信登录状态
|
|
|
+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 接口调用示例
|
|
|
+```php
|
|
|
+// 在需要微信登录的接口中添加中间件
|
|
|
+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 数据库设计
|
|
|
+```sql
|
|
|
+-- 支付订单表
|
|
|
+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 配置文件
|
|
|
+```php
|
|
|
+// 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 支付服务类
|
|
|
+```php
|
|
|
+// 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 控制器实现
|
|
|
+
|
|
|
+```php
|
|
|
+// 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 路由配置
|
|
|
+```php
|
|
|
+// 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 前端调用示例
|
|
|
+```javascript
|
|
|
+// 发起支付
|
|
|
+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. 定期对账
|