├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── database └── migrations │ ├── 2020_02_06_000000_create_wechat_cards_table.php │ ├── 2020_02_06_000000_create_wechat_configs_table.php │ ├── 2020_02_06_000000_create_wechat_merchants_table.php │ ├── 2020_02_06_000000_create_wechat_orders_table.php │ └── 2020_02_06_000000_create_wechat_users_table.php ├── resources ├── assets │ ├── css │ │ └── menu.min.css │ └── js │ │ └── menu.min.js └── views │ ├── dropdown.blade.php │ └── menu.blade.php ├── routes ├── wechat_admin.php └── wechat_api.php └── src ├── Actions ├── ImportCards.php └── ImportUsers.php ├── Console └── Commands │ ├── CreateMenu.php │ └── Install.php ├── Events ├── DecryptMobile.php ├── DecryptUserInfo.php └── OrderPaid.php ├── Exceptions ├── PaymentException.php └── WechatException.php ├── Facades ├── ConfigService.php ├── MerchantService.php └── OrderService.php ├── Fields └── MenuFormField.php ├── Http └── Controllers │ ├── Admin │ ├── BaseController.php │ ├── ConfigController.php │ ├── MiniProgram │ │ └── UserController.php │ ├── OfficialAccount │ │ ├── CardController.php │ │ ├── MenuController.php │ │ └── UserController.php │ └── Payment │ │ └── MerchantController.php │ └── Api │ ├── Mini │ └── AuthController.php │ └── Payment │ └── OrderController.php ├── Jobs ├── ImportCards.php └── ImportUsers.php ├── Models ├── WechatCard.php ├── WechatConfig.php ├── WechatFake.php ├── WechatMerchant.php ├── WechatOrder.php └── WechatUser.php ├── Services ├── ConfigService.php ├── MerchantService.php ├── MiniService.php └── OrderService.php ├── Wechat.php ├── WechatServiceProvider.php └── helpers.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hanson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel-admin-wechat 2 | 3 | Laravel admin 的微信扩展、支持多公众号、多小程序、多微信支付的后台管理,并提供小程序、微信支付的基础接口,在此基础上通过[事件](https://learnku.com/docs/laravel/6.x/events/5162)、继承等形式完成自定义。 4 | 5 | 本扩展使用了 [EasyWeChat](https://www.easywechat.com/),微信实例使用可移步到 [EasyWeChat 文档](https://www.easywechat.com/docs) 6 | 7 | ![3@6ZH77BL2I_PKOASD~6T2T.png](https://i.loli.net/2020/02/07/2jqXJfyWFTxmK7S.png) 8 | 9 | ![H__`P5CEM2W5KK_HJ_R2_3D.png](https://i.loli.net/2020/02/07/ckf1IRLYGZsS6HK.png) 10 | 11 | ![~IOO_9`PNP__04333MJ1_97.png](https://i.loli.net/2020/02/08/5Zhk1mYo7OClujP.png) 12 | 13 | ## 关联项目 14 | 15 | * [让你的 laravel-admin 更好看](https://github.com/Hanson/rainbow) 16 | * [wepy 小程序开发模板](https://github.com/Hanson/wepy-template) 17 | 18 | ## TO DO LIST 19 | 20 | - [x] 公众号与小程序配置 21 | - [ ] 公众号用户 22 | - [x] 列表用户 23 | - [x] 同步用户 24 | - [ ] 备注用户 25 | - [ ] 用户标签 26 | - [x] 公众号菜单 27 | - [x] 小程序用户 28 | - [x] 微信支付配置 29 | - [ ] 公众号卡券 30 | - [x] 卡券列表 31 | - [x] 同步卡券 32 | - [ ] 创建卡券 33 | - [ ] 投放卡券 34 | - [ ] 微信支付 35 | - [x] 微信支付配置 36 | - [x] js sdk 生成 37 | - [ ] 微信支付订单 38 | - [ ] 微信支付退款 39 | - [ ] 微信支付红包 40 | - [ ] 公众号授权 41 | - [ ] 公众号登录 42 | - [ ] 自动保存用户 43 | - [ ] 中间件 44 | - [ ] 公众号门店 45 | - [ ] 公众号模板消息 46 | - [ ] 公众号素材 47 | - [ ] 公众号客服 48 | - [ ] 开放平台 49 | - [ ] 小程序其他解密接口 50 | 51 | ## 安装 52 | 53 | 安装依赖 54 | 55 | `composer require hanson/laravel-admin-wechat -vvv` 56 | 57 | 安装 58 | 59 | `php artisan wechat:install -m` 60 | 61 | 此命令将: 62 | * 发布 WeChat 所需资源 63 | * 生成微信相关后台菜单 64 | * 创建各个微信相关数据表 65 | * 创建路由文件 `routes/wechat_admin` 与 `routes/wechat_api` 66 | * 创建 `database/migrations` 的相关微信数据库(可自行根据需求做对应修改,可以加字段,不建议删减字段) 67 | * 执行 `migrate` 操作(去掉 `-m` 可不执行) 68 | 69 | 生成 jwt secret 70 | 71 | `php artisan jwt:secret` 72 | 73 | ## 配置 74 | 75 | 修改 `config/auth.php` (用于小程序登录等接口,如果不需要可以不加) 76 | 77 | ```php 78 | [ 82 | // ... 83 | 'mini' => [ 84 | 'driver' => 'jwt', 85 | 'provider' => 'wechat_user', 86 | ] 87 | ], 88 | 'providers' => [ 89 | // ... 90 | 'wechat_user' => [ 91 | 'driver' => 'eloquent', 92 | 'model' => Hanson\LaravelAdminWechat\Models\WechatUser::class, // 你也可以自己继承此 model 后修改为自己的 model 93 | ], 94 | ] 95 | ]; 96 | ``` 97 | 98 | ## 接口 99 | 100 | 对于本人来说, `laravel-admin-wechat` 另一个有价值的点在于自带的接口,尽管内容不多,但因为做项目比较多经常要新建用户表,写登录逻辑,但实际上代码基本都一样,这也是为什么会提供基础的接口 101 | 102 | * `post` `api/wechat/mini/check-token` 检查token是否过期 103 | 104 | * `post` `api/wechat/mini/login` 使用 code 登录 105 | 106 | | 参数 | 备注 | 107 | |---|---| 108 | | app_id | 小程序的 app id | 109 | | code | 登录的 code | 110 | 111 | * `post` `api/wechat/mini/decrypt-mobile` 解密手机号码 112 | 113 | * `post` `api/wechat/mini/decrypt-user-info` 解密用户信息 114 | 115 | | 参数 | 备注 | 116 | |---|---| 117 | | app_id | 小程序的 app id | 118 | | iv | 微信参数 | 119 | | encrypted_data | 微信参数 | 120 | 121 | ## 高级 122 | 123 | 此扩展只提供了最基础的业务,但很多情况下企业需要更多样化的业务需求,`laravel-admin-wechat` 同样提供了十分灵活的自定义方案。 124 | 125 | ### 自定义后台 126 | 127 | 后台路由在 `routes/wechat_admin.php` 中,你可以自由修改 128 | 129 | 当你需要对进行细微调整时,可以通过 `php artisan admin:controller` 自行创建控制器,并修改其继承的类为原来的类,覆盖方法做调整 130 | 131 | ### 通用方法 132 | 133 | `laravel-admin-wechat` 的通用函数均在 `Hanson\LaravelAdminWechat\Services` 内,并提供 `Facade` 方式进行调用 134 | 135 | ```php 136 | [ 165 | 'App\Listeners\AfterSaveUserInfo', 166 | ], 167 | \Hanson\LaravelAdminWechat\Events\DecryptMobile::class => [ 168 | 'App\Listeners\SaveMobile', 169 | ], 170 | \Hanson\LaravelAdminWechat\Events\OrderPaid::class => [ 171 | 'App\Listeners\ChangeOrderStatus', 172 | ] 173 | ]; 174 | ``` 175 | 176 | ```php 177 | wechatUser->user()->update([ 185 | 'phone' => $event->decryptedData['purePhoneNumber'], 186 | 'country_code' => $event->decryptedData['countryCode'], 187 | ]); 188 | } 189 | } 190 | ``` 191 | 192 | ```php 193 | decryptedData['nickname']; 202 | $event->wechatUser; 203 | } 204 | } 205 | ``` 206 | 207 | ```php 208 | order; 217 | 218 | $order = $wechatOrder->order()->update(['status' => 'paid']); 219 | 220 | $openId = $wechatOrder->openid; 221 | } 222 | } 223 | ``` 224 | 225 | ### 微信支付 226 | 227 | `laravel-admin-wechat` 提供了微信订单表、创建订单以及生成 js 参数等方法,但并没有相关业务参数 `地址`、`商品` 等,建议自身生成 `orders` 表并关联 `wechat_orders` 228 | 229 | ```php 230 | // 支付接口示例 231 | '商品标题', 241 | 'total_fee' => 100, 242 | 'openid' => auth('mini')->user()->openid, 243 | // 'out_trade_no' => 'xxx', 选填,如不填写时会自动创建一个订单号 244 | ]; 245 | 246 | /** 247 | * $result['config'] jssdk 所需参数 248 | * $result['order'] WechatOrder 的 model 对象 249 | * $result['unify'] unify 接口返回的结果 250 | */ 251 | $result = \Hanson\LaravelAdminWechat\Facades\OrderService::jsConfig('mch id', 'JSAPI', $data); 252 | 253 | App\Models\Order::create([ 254 | 'wechat_order_id' => $result['order']->id, 255 | 'status' => 'not paid', 256 | 'goods_id' => '...', 257 | 'address_id' => '...', 258 | ]); 259 | 260 | return $result; 261 | } 262 | } 263 | ``` 264 | 265 | ### 异常捕捉 266 | 267 | 不但在此项目,在其他项目也建议你对 `app/Exceptions/Handler` 进行修改,因为对于接口的请求,laravel 默认在错误的情况会返回页面,这不是任何一个开发者所期望的情况 268 | 269 | ```php 270 | // app/Exceptions/Handler.php 271 | acceptsJson()) { 276 | if ($exception instanceof AuthenticationException) { 277 | return Response::json(['err_code' => 401, 'err_msg' => 'Unauthenticated'], 401); 278 | } 279 | return fail($exception->getMessage()); 280 | } 281 | return parent::render($request, $exception); 282 | } 283 | ``` 284 | 285 | 本扩展封装了函数 `fail` 和 `ok` 两个接口返回的基础结构,可以使用这两个函数去定义你所有的接口返回 286 | 287 | ## 特别鸣谢 288 | 289 | [EasyWeChat 微信开发包](https://github.com/overtrue/wechat) 290 | 291 | [yisonli/wxmenu 微信菜单的代码来源](https://github.com/yisonli/wxmenu) 292 | 293 | ## 定制 294 | 295 | 如需找我定制,可加我微信 524291355 296 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hanson/laravel-admin-wechat", 3 | "authors": [ 4 | { 5 | "name": "Hanson", 6 | "email": "h@hanc.cc" 7 | } 8 | ], 9 | "require": { 10 | "overtrue/wechat": "^4.2", 11 | "tymon/jwt-auth": "^1.0" 12 | }, 13 | "require-dev": { 14 | "encore/laravel-admin": "^1.7" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Hanson\\LaravelAdminWechat\\": "src/" 19 | }, 20 | "files": [ 21 | "src/helpers.php" 22 | ] 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "Hanson\\LaravelAdminWechat\\WechatServiceProvider" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2020_02_06_000000_create_wechat_cards_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('app_id'); 19 | $table->string('card_type')->comment('类型'); 20 | $table->string('card_id')->comment('微信卡券id'); 21 | $table->string('logo_url', 128)->comment('卡券的商户logo'); 22 | $table->string('code_type', 16)->comment('码型'); 23 | $table->string('brand_name')->comment('商户名字'); 24 | $table->string('title')->comment('卡券名'); 25 | $table->string('color', 16)->comment('券颜色'); 26 | $table->string('notice')->comment('卡券使用提醒'); 27 | $table->string('description')->comment('卡券使用说明'); 28 | $table->json('sku')->comment('商品信息'); 29 | $table->json('date_info')->comment('使用日期'); 30 | $table->timestamps(); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('wechat_cards'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2020_02_06_000000_create_wechat_configs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->unsignedTinyInteger('type')->comment('1-公众号 2-小程序'); 20 | $table->string('app_id', 32)->unique(); 21 | $table->string('secret'); 22 | $table->string('token')->nullable(); 23 | $table->string('aes_key')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('wechat_configs'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_02_06_000000_create_wechat_merchants_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->unsignedTinyInteger('type')->default(1)->comment('1-普通商户号 2-服务商'); 20 | $table->string('mch_id', 32)->unique(); 21 | $table->string('app_id'); 22 | $table->string('key'); 23 | $table->string('cert_path')->nullable(); 24 | $table->string('key_path')->nullable(); 25 | $table->string('notify_url')->nullable(); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('wechat_merchants'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2020_02_06_000000_create_wechat_orders_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('appid', 32); 19 | $table->string('mch_id', 32)->comment('商户号'); 20 | $table->string('device_info', 32)->nullable(); 21 | $table->string('body', 128)->comment('商品描述'); 22 | $table->string('detail', 6000)->nullable()->comment('商品详情'); 23 | $table->string('attach', 32)->nullable()->comment('附加数据'); 24 | $table->string('out_trade_no', 32)->comment('商户订单号'); 25 | $table->string('fee_type', 16)->default('CNY')->comment('标价币种'); 26 | $table->unsignedInteger('total_fee')->comment('标价金额'); 27 | $table->string('goods_tag', 32)->nullable()->comment('订单优惠标记'); 28 | $table->string('product_id', 32)->nullable()->comment('商品ID'); 29 | $table->string('openid', 128)->nullable()->comment('用户标识'); 30 | $table->timestamp('paid_at')->nullable()->comment('支付时间'); 31 | $table->timestamps(); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('wechat_orders'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/2020_02_06_000000_create_wechat_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('app_id', 32); 19 | $table->unsignedBigInteger('user_id')->default(0); 20 | $table->string('openid', 32)->index(); 21 | $table->string('nickname')->nullable(); 22 | $table->string('avatar')->nullable(); 23 | $table->unsignedTinyInteger('gender')->default(0); 24 | $table->string('country')->nullable(); 25 | $table->string('province')->nullable(); 26 | $table->string('city')->nullable(); 27 | $table->timestamp('subscribed_at')->nullable(); 28 | $table->timestamps(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('wechat_users'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resources/assets/css/menu.min.css: -------------------------------------------------------------------------------- 1 | .editor-handler-tips{position:absolute;left:50%;top:50%;margin:-10px 0 0 -10px;color:#07a4ef}.custom-menu-wraper{padding:20px 25px}.custom-menu-wraper h2,.custom-menu-wraper p,.custom-menu-wraper ul,.custom-menu-wraper h3{margin:0;padding:0}.custom-phone-box{min-width:320px;max-width:320px;background:#fff;height:565px;box-shadow:2px 3px 5px #e7e7eb;border-radius:2px;border:1px solid #e7e7eb;text-align:center}.custom-nav-title{line-height:55px;background:#333;border-radius:2px 2px 0 0;color:#fff;font-size:15px;margin:0}.custom-phone-body{line-height:457px;margin:0;color:#999}.custom-phone-footer{border-top:1px solid #e7e7eb;line-height:50px;background:#fbfbfb}.custom-a-button{display:block;line-height:50px;text-align:center}.custom-a-button:hover{text-decoration:none}.custom-svg-size{display:inline-block;width:25px;height:25px;margin:0;padding:0;vertical-align:top;position:relative}.custom-svg-size:after,.custom-svg-size::before{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:23px;height:3px;border-radius:10px;background:#666}.custom-svg-size::before{height:23px;width:3px}.custom-a-button .custom-svg-size{vertical-align:-9px}.custom-a-button:hover .custom-svg-size::before,.custom-a-button:hover .custom-svg-size::after{background:#1b8bf5}.custom-edit-menu{background-color:#f4f5f9;border:1px solid #e7e7eb;padding:0 20px 5px;min-height:565px;position:relative;border-radius:2px}.custom-editor_arrow_wrp{position:absolute;left:-13px;bottom:40px}.custom-editor_arrow_wrp .editor_arrow{display:inline-block;width:0;height:0;border-width:12px;border-style:dashed;border-color:transparent;border-left-width:0;border-right-color:#e7e7eb;border-right-style:solid;left:0;position:absolute}.custom-editor_arrow_wrp .editor_arrow_in{left:1px;border-right-color:#f4f5f9}.custom-editor-title{padding-top:10px;border-bottom:1px solid #e6e6e6;margin:0;line-height:40px}.custom-edit-menu h2{font-size:15px;line-height:40px;font-weight:normal}.form-horizontal{padding-top:20px}.form-horizontal .control-label{font-weight:normal;padding-right:0}.menu-type-select{margin-right:10px;display:inline-block}.custom-panel-default{background:#fff;border-radius:3px;padding:25px 5px;border:1px solid #e6e6e6}.form-horizontal .submit-button{margin-top:40px}.custom-menu-wraper .submit-menu-handler{padding:20px 0}.pre_menu_list{display:box;display:-webkit-box;display:flex;display:-webkit-flex;margin:0;padding:0}.pre_menu_list li{list-style:none;flex:1;border:1px solid #fbfbfb;transition:all .3s;position:relative}.pre_menu_item+.pre_menu_item{border-left:1px solid #e7e7eb;font-size:12px}.pre_menu_item a{color:#444}.pre_menu_item.active{border-color:#07a4ef}.pre_menu_item.active a.pre_menu_link{color:#07a4ef}.pre_menu_item.selected .sub_pre_menu_box{display:block}.sub_pre_menu_box{display:none;position:absolute;bottom:63px;left:2.5%;width:95%;border:1px solid #e7e7eb;background-color:#fff}.sub_pre_menu_box .arrow{position:absolute;left:50%;margin-left:-6px;display:inline-block;width:0;height:0;border-style:dashed;border-color:transparent;border-bottom-width:0;border-top-style:solid;border-width:6px}.sub_pre_menu_box .arrow_out{border-top-color:#e7e7eb;bottom:-13px}.sub_pre_menu_box .arrow_in{bottom:-12px;border-top-color:#fff}.sub_pre_menu_box a{line-height: 22px;color: #444;font-size: 13px;padding: 8px 2px;}.sub_pre_menu_item+.sub_pre_menu_item{border-top:1px solid #e7e7eb}.sub_pre_menu_item .custom-a-button:hover{background:#fbfbfb;border-color:#fbfbfb}.sub_pre_menu_item.active .custom-a-button{background:#07a4ef;color:#fff}.custom-toast{margin-top:20px;color:#fb000b;font-weight:bold} 2 | -------------------------------------------------------------------------------- /resources/assets/js/menu.min.js: -------------------------------------------------------------------------------- 1 | var time=null;function VueInit(t,e,rootNode){new Vue({el:rootNode,data:{toastMsg:"",zindex:-1,subZindex:-1,nowItem:{},resultJson:JSON.stringify({menu:t||{}},null," "),menu:t,menuTypeList:[{value:"view",label:"跳转链接"},{value:"click",label:"事件推送"},{value:"miniprogram",label:"跳转小程序"}]},methods:{toast:function(t){clearTimeout(time),this.toastMsg=t;var e=this;time=setTimeout(function(){e.toastMsg=""},2e3)},addMenuHandler:function(t){if(0==t)this.menu.button.push({name:"菜单名称",type:"view"});else if(1==t){var e={name:"子菜单名称",type:"view"};this.menu.button[this.zindex].hasOwnProperty("sub_button")?(this.menu.button[this.zindex].sub_button.push(e),this.nowItem=deepClone(this.menu.button[this.zindex])):(Vue.set(this.menu.button[this.zindex],"sub_button",[e]),Vue.set(this.nowItem,"sub_button",[e])),Vue.delete(this.menu.button[this.zindex],"url"),Vue.delete(this.menu.button[this.zindex],"type"),Vue.delete(this.menu.button[this.zindex],"appid")}},removeMenuHandler:function(){this.subZindex<0?(this.menu.button.splice(this.zindex,1),this.zindex=-1,this.nowItem={}):(this.menu.button[this.zindex].sub_button.splice(this.subZindex,1),this.subZindex=-1,this.nowItem=deepClone(this.menu.button[this.zindex])),this.resultJson=JSON.stringify({menu:this.menu})},selectedMenuItem:function(t){this.zindex=t,this.nowItem=deepClone(this.menu.button[t]),this.subZindex=-1},selectedSubMenuItem:function(t){this.subZindex=t,this.nowItem=deepClone(this.menu.button[this.zindex].sub_button[t])},confirmEditor:function(){if(""==this.nowItem.name)return this.toast("请填写菜单名称"),!1;if("view"==this.nowItem.type&&!this.nowItem.url)return this.toast("请输入跳转链接"),!1;if("click"==this.nowItem.type&&!this.nowItem.key)return this.toast("请输入事件KEY"),!1;if("miniprogram"==this.nowItem.type){if(!this.nowItem.pagepath)return this.toast("请输入小程序路径"),!1;if(!this.nowItem.url)return this.toast("请输入备用链接"),!1;if(!this.nowItem.appid)return this.toast("请输入AppId"),!1}this.subZindex<0?this.menu.button.splice(this.zindex,1,this.nowItem):this.menu.button[this.zindex].sub_button.splice(this.subZindex,1,this.nowItem),this.resultJson=JSON.stringify({menu:this.menu},null," "),this.toast("修改成功")},saveHandler:function(){e&&e({menu:this.menu})},selectedMenuType:function(t){var e={type:t,name:this.nowItem.name};"click"==t?e.key=this.nowItem.url||"":"view"==t?e.url=this.nowItem.url||"":(e.appid="",e.url=this.nowItem.url||"",e.pagepath=""),this.nowItem=e}}})}function deepClone(t){var e;if(isArray(t))e=[];else{if(!isObject(t))return t;e={}}if(isArray(t))for(var n=0,i=t.length;n 2 | .dropdown-item { 3 | display: block; 4 | padding: .25rem 1.5rem; 5 | font-weight: 400; 6 | } 7 | .dropdown-item:hover { 8 | color: #0081ff; 9 | background: #eee; 10 | } 11 | .nav-link .dropdown-toggle:hover { 12 | color: #0081ff; 13 | } 14 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /resources/views/menu.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

{{$label}}

6 |

内容区域

7 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 |
54 |
55 |

编辑菜单

56 | 删除菜单 57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 |
67 | 68 |
69 | 76 |
77 |
78 | 79 |
80 | 81 |
82 |
83 |
84 | 85 |

86 | 87 |

88 |
89 |
90 | 91 |

92 | 93 |

94 |
95 |
96 | 97 |

98 | 99 |

100 |
101 |
102 |
103 |
104 | 105 |

106 | 107 |

108 |
109 |
110 |
111 |
112 | 113 |

114 | 116 |

117 |
118 |
119 |
120 |
121 |
122 | 123 |
124 | 125 |
126 | 129 |
130 | 131 |
132 |
133 |
134 |
135 |
136 |

请选择左侧菜单

137 |
138 |
139 |
140 | 141 | 142 |
143 | 144 | 153 | -------------------------------------------------------------------------------- /routes/wechat_admin.php: -------------------------------------------------------------------------------- 1 | config('admin.route.prefix').'/wechat', 7 | 'namespace' => 'Hanson\\LaravelAdminWechat\\Http\\Controllers\\Admin', 8 | 'middleware' => config('admin.route.middleware'), 9 | ], function () { 10 | Route::resources([ 11 | 'configs' => 'ConfigController', 12 | ]); 13 | 14 | // 公众号操作 15 | Route::group(['prefix' => 'official-account', 'namespace' => 'OfficialAccount'], function () { 16 | Route::resource('cards', 'CardController'); 17 | Route::resource('users', 'UserController', ['as' => 'official-account']); 18 | Route::get('menu', 'MenuController@index')->name('admin.wechat.menu'); 19 | Route::post('menu', 'MenuController@store')->name('admin.wechat.menu.update'); 20 | 21 | }); 22 | 23 | // 小程序操作 24 | Route::group(['prefix' => 'mini-program', 'namespace' => 'MiniProgram'], function () { 25 | Route::resource('users', 'UserController', ['as' => 'mini-program']); 26 | }); 27 | 28 | // 支付操作 29 | Route::group(['prefix' => 'payment', 'namespace' => 'Payment'], function () { 30 | Route::resources([ 31 | 'merchants' => 'MerchantController', 32 | ]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /routes/wechat_api.php: -------------------------------------------------------------------------------- 1 | 'api/wechat/mini', 11 | 'namespace' => 'Hanson\\LaravelAdminWechat\\Http\\Controllers\\Api\\Mini', 12 | ], function () { 13 | Route::post('login', 'AuthController@login'); 14 | 15 | Route::middleware('auth:mini')->group(function () { 16 | Route::post('check-token', 'AuthController@checkToken'); 17 | Route::post('decrypt-user-info', 'AuthController@decryptUserInfo'); 18 | Route::post('decrypt-mobile', 'AuthController@decryptMobile'); 19 | }); 20 | }); 21 | 22 | /** 23 | * 支付相关的接口 24 | */ 25 | Route::group([ 26 | 'prefix' => 'api/wechat/payment', 27 | 'namespace' => 'Hanson\\LaravelAdminWechat\\Http\\Controllers\\Api\\Payment', 28 | ], function () { 29 | Route::post('paid-notify', 'OrderController@paidNotify'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/Actions/ImportCards.php: -------------------------------------------------------------------------------- 1 | app_id); 19 | 20 | return $this->response()->success('后台同步卡券中,请耐心等待')->refresh(); 21 | } 22 | 23 | public function html() 24 | { 25 | return << 同步卡券 27 | HTML; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Actions/ImportUsers.php: -------------------------------------------------------------------------------- 1 | response()->success('后台同步用户中,请耐心等待')->refresh(); 25 | } 26 | 27 | public function html() 28 | { 29 | return << 同步用户 31 | HTML; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Console/Commands/CreateMenu.php: -------------------------------------------------------------------------------- 1 | firstOrCreate([ 32 | 'title' => '微信管理', 33 | ],[ 34 | 'parent_id' => 0, 35 | 'order' => 80, 36 | 'icon' => 'fa-wechat', 37 | ]); 38 | 39 | Menu::query()->firstOrCreate([ 40 | 'title' => '微信配置', 41 | ],[ 42 | 'parent_id' => $menu->id, 43 | 'order' => 80, 44 | 'icon' => 'fa-cog', 45 | 'uri' => 'wechat/configs' 46 | ]); 47 | 48 | $officialAccountMenu = Menu::query()->firstOrCreate([ 49 | 'title' => '公众号', 50 | ],[ 51 | 'parent_id' => $menu->id, 52 | 'order' => 80, 53 | 'icon' => '', 54 | ]); 55 | 56 | $miniProgramMenu = Menu::query()->firstOrCreate([ 57 | 'title' => '小程序', 58 | ],[ 59 | 'parent_id' => $menu->id, 60 | 'order' => 80, 61 | 'icon' => '', 62 | ]); 63 | 64 | $paymentMenu = Menu::query()->firstOrCreate([ 65 | 'title' => '微信支付', 66 | ],[ 67 | 'parent_id' => $menu->id, 68 | 'order' => 80, 69 | 'icon' => '', 70 | ]); 71 | 72 | // 公众号 73 | Menu::query()->firstOrCreate([ 74 | 'title' => '用户', 75 | 'parent_id' => $officialAccountMenu->id, 76 | ],[ 77 | 'order' => 80, 78 | 'icon' => 'fa-user', 79 | 'uri' => 'wechat/official-account/users' 80 | ]); 81 | 82 | Menu::query()->firstOrCreate([ 83 | 'title' => '菜单', 84 | 'parent_id' => $officialAccountMenu->id, 85 | ],[ 86 | 'order' => 80, 87 | 'icon' => 'fa-bars', 88 | 'uri' => 'wechat/official-account/menu' 89 | ]); 90 | 91 | Menu::query()->firstOrCreate([ 92 | 'title' => '卡券', 93 | 'parent_id' => $officialAccountMenu->id, 94 | ],[ 95 | 'order' => 80, 96 | 'icon' => 'fa-credit-card-alt', 97 | 'uri' => 'wechat/official-account/cards' 98 | ]); 99 | 100 | // 小程序 101 | Menu::query()->firstOrCreate([ 102 | 'title' => '用户', 103 | 'parent_id' => $miniProgramMenu->id, 104 | ],[ 105 | 'order' => 80, 106 | 'icon' => 'fa-user', 107 | 'uri' => 'wechat/official-account/users' 108 | ]); 109 | 110 | // 微信支付 111 | Menu::query()->firstOrCreate([ 112 | 'title' => '商户号', 113 | 'parent_id' => $paymentMenu->id, 114 | ],[ 115 | 'order' => 80, 116 | 'icon' => 'fa-user', 117 | 'uri' => 'wechat/payment/merchants' 118 | ]); 119 | 120 | 121 | $this->info('菜单生成完毕'); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Console/Commands/Install.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', ['--tag' => 'laravel-admin-wechat', '--force' => true]); 32 | 33 | $this->call('wechat:menu'); 34 | 35 | if ($this->option('migrate')) { 36 | $this->call('migrate', ['--force' => true]); 37 | } 38 | 39 | $this->info('wechat 扩展安装完毕'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Events/DecryptMobile.php: -------------------------------------------------------------------------------- 1 | decryptedData = $decryptedData; 37 | $this->wechatUser = $wechatUser; 38 | } 39 | 40 | /** 41 | * Get the channels the event should broadcast on. 42 | * 43 | * @return \Illuminate\Broadcasting\Channel|array 44 | */ 45 | public function broadcastOn() 46 | { 47 | return new PrivateChannel('channel-name'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Events/DecryptUserInfo.php: -------------------------------------------------------------------------------- 1 | decryptedData = $decryptedData; 37 | $this->wechatUser = $wechatUser; 38 | } 39 | 40 | /** 41 | * Get the channels the event should broadcast on. 42 | * 43 | * @return \Illuminate\Broadcasting\Channel|array 44 | */ 45 | public function broadcastOn() 46 | { 47 | return new PrivateChannel('channel-name'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Events/OrderPaid.php: -------------------------------------------------------------------------------- 1 | order = $order; 28 | } 29 | 30 | /** 31 | * Get the channels the event should broadcast on. 32 | * 33 | * @return \Illuminate\Broadcasting\Channel|array 34 | */ 35 | public function broadcastOn() 36 | { 37 | return new PrivateChannel('channel-name'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/PaymentException.php: -------------------------------------------------------------------------------- 1 | default)) { 24 | $this->value = [ 25 | "button" => [], 26 | ]; 27 | } else { 28 | $this->value = $this->default; 29 | } 30 | 31 | if (is_array($this->value)) { 32 | $this->value = json_encode($this->value, JSON_UNESCAPED_UNICODE); 33 | } 34 | 35 | return parent::render(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/BaseController.php: -------------------------------------------------------------------------------- 1 | config = $current; 38 | $this->appId = $current->app_id; 39 | 40 | Admin::navbar(function (\Encore\Admin\Widgets\Navbar $navbar) use ($current) { 41 | $configs = WechatConfig::query()->get(['app_id', 'name']); 42 | 43 | $navbar->left(view('wechat::dropdown', compact('configs', 'current'))); 44 | }); 45 | 46 | return $content 47 | ->title($this->title()) 48 | ->description($this->description['index'] ?? trans('admin.list')) 49 | ->body($this->grid()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/ConfigController.php: -------------------------------------------------------------------------------- 1 | '公众号/小程序 配置', 19 | ]; 20 | 21 | protected function grid() 22 | { 23 | $grid = new Grid(new WechatConfig); 24 | 25 | $grid->column('id', __('ID'))->sortable(); 26 | $grid->column('name', '名称'); 27 | $grid->column('type_readable', '类型'); 28 | $grid->column('app_id', 'APP ID'); 29 | $grid->column('created_at', __('Created at')); 30 | $grid->column('updated_at', __('Updated at')); 31 | 32 | return $grid; 33 | } 34 | 35 | protected function form() 36 | { 37 | $form = new Form(new WechatConfig()); 38 | 39 | $form->text('name', '名称')->required(); 40 | $form->radio('type', '类型')->default(1)->options([1 => '公众号', 2 => '小程序']); 41 | $form->text('app_id', 'App id')->required(); 42 | $form->text('secret', '秘钥')->required(); 43 | $form->text('token', 'Token')->help('公众号才需填写'); 44 | $form->text('aes_key', 'Aes Key')->help('公众号才需填写'); 45 | 46 | $form->saved(function (Form $form) { 47 | Cache::forever('wechat.config.app_id.'.$form->model()->app_id, ['app_id' => $form->model()->app_id, 'secret' => $form->model()->secret, 'type' => $form->model()->type]); 48 | }); 49 | 50 | return $form; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/MiniProgram/UserController.php: -------------------------------------------------------------------------------- 1 | model()->where('app_id', $this->appId); 26 | 27 | $grid->column('id', __('ID'))->sortable(); 28 | $grid->column('app_id', 'App Id'); 29 | $grid->column('openid', 'Openid'); 30 | $grid->column('nickname', '昵称'); 31 | $grid->column('avatar', '头像')->image('', 100, 100); 32 | $grid->column('gender_readable', '性别'); 33 | $grid->column('country', '国家'); 34 | $grid->column('province', '省份'); 35 | $grid->column('city', '城市'); 36 | 37 | $grid->disableCreateButton(); 38 | $grid->disableActions(); 39 | 40 | return $grid; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/OfficialAccount/CardController.php: -------------------------------------------------------------------------------- 1 | column('id', __('Id')); 33 | $grid->column('logo_url', '卡券的商户logo')->image('', 64, 64); 34 | $grid->column('card_type_readable', '类型'); 35 | $grid->column('card_id', '微信卡券id')->copyable(); 36 | $grid->column('code_type_readable', '码型'); 37 | $grid->column('brand_name', '商户名字'); 38 | $grid->column('title', '卡券名'); 39 | $grid->column('sku', '商品信息')->display(function () { 40 | $sku = []; 41 | if(isset($this->sku['total_quantity'])) $sku[] = '卡券全部库存的数量: '.$this->sku['total_quantity']; 42 | if(isset($this->sku['quantity'])) $sku[] = '卡券现有库存的数量: '.$this->sku['quantity']; 43 | if ($sku) { 44 | return implode('
', $sku); 45 | } 46 | }); 47 | $grid->column('date_info', '使用日期')->display(function () { 48 | $date = []; 49 | $date[] = '时间的类型: '.WechatCard::DATE_INFO_TYPE_MAP[$this->date_info['type']]; 50 | if(isset($this->date_info['begin_timestamp'])) $date[] = '起用时间: '.Carbon::parse($this->date_info['begin_timestamp'])->toDateTimeString(); 51 | if(isset($this->date_info['end_timestamp'])) $date[] = '结束时间: '.Carbon::parse($this->date_info['end_timestamp'])->toDateTimeString(); 52 | if(isset($this->date_info['fixed_term'])) $date[] = '领取后多少天内有效: '.$this->date_info['fixed_term']; 53 | if(isset($this->date_info['fixed_begin_term'])) $date[] = '领取后多少天开始生效: '.$this->date_info['fixed_begin_term']; 54 | if ($date) { 55 | return implode('
', $date); 56 | } 57 | }); 58 | 59 | $grid->tools(function (Grid\Tools $tools) { 60 | $tools->append(new ImportCards()); 61 | }); 62 | 63 | return $grid; 64 | } 65 | 66 | /** 67 | * Make a show builder. 68 | * 69 | * @param mixed $id 70 | * @return Show 71 | */ 72 | protected function detail($id) 73 | { 74 | $show = new Show(WechatCard::findOrFail($id)); 75 | 76 | $show->field('id', __('Id')); 77 | $show->field('card_type', __('Card type')); 78 | $show->field('card_id', __('Card id')); 79 | $show->field('logo_url', __('Logo url')); 80 | $show->field('code_type', __('Code type')); 81 | $show->field('brand_name', __('Brand name')); 82 | $show->field('title', __('Title')); 83 | $show->field('color', __('Color')); 84 | $show->field('notice', __('Notice')); 85 | $show->field('description', __('Description')); 86 | $show->field('sku', __('Sku')); 87 | $show->field('date_info', __('Date info')); 88 | $show->field('created_at', __('Created at')); 89 | $show->field('updated_at', __('Updated at')); 90 | 91 | return $show; 92 | } 93 | 94 | /** 95 | * Make a form builder. 96 | * 97 | * @return Form 98 | */ 99 | protected function form() 100 | { 101 | $form = new Form(new WechatCard()); 102 | 103 | $form->text('card_type', __('Card type')); 104 | $form->text('card_id', __('Card id')); 105 | $form->text('logo_url', __('Logo url')); 106 | $form->text('code_type', __('Code type')); 107 | $form->text('brand_name', __('Brand name')); 108 | $form->text('title', __('Title')); 109 | $form->color('color', __('Color')); 110 | $form->text('notice', __('Notice')); 111 | $form->text('description', __('Description')); 112 | $form->text('sku', __('Sku')); 113 | $form->text('date_info', __('Date info')); 114 | 115 | return $form; 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/OfficialAccount/MenuController.php: -------------------------------------------------------------------------------- 1 | menu->create(json_decode(request('menu'), true)['menu']['button']); 22 | } 23 | 24 | $config = ConfigService::getCurrent(); 25 | 26 | $app = ConfigService::getAdminCurrentApp(); 27 | 28 | $menu = $app->menu->current(); 29 | 30 | $form = new Form(new WechatConfig()); 31 | 32 | $form->setAction('/admin/wechat/official-account/menu'); 33 | 34 | $form->wechatMenu('menu', $config->name)->default(isset($menu['selfmenu_info']) ? $menu['selfmenu_info'] : null); 35 | $form->hidden('app_id')->default($config->app_id); 36 | 37 | $form->disableViewCheck()->disableEditingCheck()->disableCreatingCheck()->disableReset(); 38 | 39 | return $form; 40 | } 41 | 42 | public function store() 43 | { 44 | $result = $this->grid(false); 45 | 46 | if ($result['errcode'] == 0) { 47 | admin_toastr('修改成功', 'success'); 48 | } else { 49 | admin_toastr($result['errmsg'], 'error'); 50 | } 51 | 52 | return redirect()->route('admin.wechat.menu'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/OfficialAccount/UserController.php: -------------------------------------------------------------------------------- 1 | model()->where('app_id', $this->appId); 21 | 22 | $grid->column('id', __('ID'))->sortable(); 23 | $grid->column('avatar', '头像')->image('', 64, 64); 24 | $grid->column('app_id', 'App Id'); 25 | $grid->column('openid', 'Openid'); 26 | $grid->column('nickname', '昵称'); 27 | $grid->column('gender_readable', '性别'); 28 | $grid->column('country', '国家'); 29 | $grid->column('province', '省份'); 30 | $grid->column('city', '城市'); 31 | $grid->column('subscribed_at', '关注时间'); 32 | 33 | $grid->disableCreateButton(); 34 | $grid->disableActions(); 35 | $grid->tools(function (Grid\Tools $tools) { 36 | $tools->append(new ImportUsers()); 37 | }); 38 | 39 | return $grid; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/Controllers/Admin/Payment/MerchantController.php: -------------------------------------------------------------------------------- 1 | column('id', __('ID'))->sortable(); 27 | $grid->column('name', '名称'); 28 | $grid->column('type_readable', '类型'); 29 | $grid->column('mch_id', '商户号'); 30 | $grid->column('app_id', 'App Id'); 31 | $grid->column('key', '秘钥'); 32 | $grid->column('notify_url', '回调地址'); 33 | 34 | $grid->disableActions(); 35 | 36 | return $grid; 37 | } 38 | 39 | protected function form() 40 | { 41 | $form = new Form(new WechatMerchant); 42 | 43 | $form->radio('type', '类型')->default(1)->options([1 => '普通商户号', 2 => '服务商']); 44 | $form->text('name', '名称')->required(); 45 | $form->text('mch_id', '商户号')->required(); 46 | $form->text('app_id', 'App Id')->required(); 47 | $form->text('key', '秘钥')->required(); 48 | $form->text('notify_url', '回调地址'); 49 | 50 | $form->saved(function (Form $form) { 51 | Cache::forever('wechat.merchant.mch_id.'.$form->model()->mch_id, ['app_id' => $form->model()->app_id, 'mch_id' => $form->model()->mch_id, 'key' => $form->model()->key, 'notify_url' => $form->model()->notify_url]); 52 | }); 53 | 54 | return $form; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Mini/AuthController.php: -------------------------------------------------------------------------------- 1 | user(); 18 | 19 | return ok($wechatUser); 20 | } 21 | 22 | public function login(Request $request, MiniService $service) 23 | { 24 | $request->validate([ 25 | 'code' => 'required', 26 | 'app_id' => 'required', 27 | ]); 28 | 29 | $session = $service->session($appId = $request->get('app_id'), $request->get('code')); 30 | 31 | $wechatUser = config('admin.extensions.wechat.wechat_user', WechatUser::class)::query()->firstOrCreate([ 32 | 'openid' => $session['openid'], 33 | 'app_id' => $appId, 34 | ]); 35 | 36 | $token = auth('mini')->login($wechatUser); 37 | 38 | return ok([ 39 | 'access_token' => 'bearer '.$token, 40 | 'expires_in' => auth('mini')->factory()->getTTL() * 60, 41 | 'wechat_user' => $wechatUser, 42 | ]); 43 | } 44 | 45 | protected function decryptMobile(Request $request, MiniService $service) 46 | { 47 | $request->validate([ 48 | 'iv' => 'required', 49 | 'encrypted_data' => 'required', 50 | 'app_id' => 'required', 51 | ]); 52 | 53 | $wechatUser = Auth::guard('mini')->user(); 54 | 55 | $decryptedData = $service->decrypt($request->get('app_id'), $wechatUser->openid, $request->get('iv'), $request->get('encrypted_data')); 56 | 57 | event(new DecryptMobile($decryptedData, $wechatUser)); 58 | 59 | return ok($decryptedData); 60 | } 61 | 62 | protected function decryptUserInfo(Request $request, MiniService $service) 63 | { 64 | $request->validate([ 65 | 'iv' => 'required', 66 | 'encrypted_data' => 'required', 67 | 'app_id' => 'required', 68 | ]); 69 | 70 | $wechatUser = Auth::guard('mini')->user(); 71 | 72 | $decryptedData = $service->decrypt($request->get('app_id'), $wechatUser->openid, $request->get('iv'), $request->get('encrypted_data')); 73 | 74 | $wechatUser->update([ 75 | 'nickname' => $decryptedData['nickName'], 76 | 'country' => $decryptedData['country'], 77 | 'province' => $decryptedData['province'], 78 | 'city' => $decryptedData['city'], 79 | 'gender' => $decryptedData['gender'], 80 | 'avatar' => $decryptedData['avatarUrl'], 81 | ]); 82 | 83 | event(new DecryptUserInfo($decryptedData, $wechatUser)); 84 | 85 | return ok($decryptedData); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Payment/OrderController.php: -------------------------------------------------------------------------------- 1 | handlePaidNotify(function ($message, $fail) { 18 | $order = WechatOrder::query()->where([ 19 | 'mch_id' => request('mch_id'), 20 | 'out_trade_no' => $message['out_trade_no'], 21 | ])->first(); 22 | 23 | if (!$order || $order->paid_at) { 24 | return true; 25 | } 26 | 27 | if ($message['return_code'] === 'SUCCESS') { // return_code 表示通信状态,不代表支付状态 28 | // 用户是否支付成功 29 | if ($message['result_code'] === 'SUCCESS') { 30 | $order->update(['paid_at' => Carbon::parse($message['time_end'])->toDateTimeString()]); 31 | 32 | event(new OrderPaid($order)); 33 | } 34 | } else { 35 | return $fail('通信失败,请稍后再通知我'); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Jobs/ImportCards.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 32 | } 33 | 34 | /** 35 | * Execute the job. 36 | * 37 | * @return void 38 | */ 39 | public function handle() 40 | { 41 | $app = ConfigService::getInstanceByAppId($this->appId); 42 | 43 | $offset = 0; 44 | 45 | while (true) { 46 | $list = $app->card->list($offset, 20, null); 47 | 48 | $offset+= 20; 49 | 50 | if ($list['errcode'] != 0) { 51 | throw new WechatException($list['errmsg']); 52 | } 53 | 54 | logger(count($list['card_id_list'])); 55 | 56 | if ($list['card_id_list']) { 57 | foreach ($list['card_id_list'] as $id) { 58 | $card = $app->card->get($id)['card']; 59 | 60 | $type = strtolower($card['card_type']); 61 | 62 | $baseInfo = $card[$type]['base_info']; 63 | 64 | WechatCard::query()->updateOrCreate([ 65 | 'card_id' => $baseInfo['id'], 66 | 'app_id' => $this->appId, 67 | ], [ 68 | 'card_type' => $card['card_type'], 69 | 'logo_url' => $baseInfo['logo_url'], 70 | 'code_type' => $baseInfo['code_type'], 71 | 'brand_name' => $baseInfo['brand_name'], 72 | 'title' => $baseInfo['title'], 73 | 'color' => $baseInfo['color'], 74 | 'notice' => $baseInfo['notice'], 75 | 'description' => $baseInfo['description'], 76 | 'sku' => $baseInfo['sku'], 77 | 'date_info' => $baseInfo['date_info'], 78 | ]); 79 | } 80 | } else { 81 | return; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Jobs/ImportUsers.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 30 | } 31 | 32 | /** 33 | * Execute the job. 34 | * 35 | * @return void 36 | */ 37 | public function handle() 38 | { 39 | $app = ConfigService::getInstanceByAppId($this->appId); 40 | 41 | $nextOpenId = null; 42 | 43 | while (true) { 44 | $list = $app->user->list($nextOpenId); 45 | 46 | $nextOpenId = $list['next_openid']; 47 | 48 | if (!$list['count']) { 49 | return; 50 | } 51 | 52 | $chunk = array_chunk($list['data']['openid'], 100); 53 | 54 | foreach ($chunk as $openids) { 55 | $result = $app->user->select($openids); 56 | 57 | foreach ($result['user_info_list'] as $user) { 58 | config('admin.extensions.wechat.wechat_user', WechatUser::class)::query()->updateOrCreate([ 59 | 'app_id' => $this->appId, 60 | 'openid' => $user['openid'], 61 | ], [ 62 | 'nickname' => $user['nickname'] ?? null, 63 | 'avatar' => $user['headimgurl'] ?? null, 64 | 'gender' => $user['sex'] ?? null, 65 | 'country' => $user['country'] ?? null, 66 | 'province' => $user['province'] ?? null, 67 | 'city' => $user['city'] ?? null, 68 | 'subscribed_at' => $user['subscribe'] ? Carbon::createFromTimestamp($user['subscribe_time'])->toDateTimeString() : null, 69 | ]); 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Models/WechatCard.php: -------------------------------------------------------------------------------- 1 | 'array', 'date_info' => 'array']; 17 | 18 | protected $appends = ['card_type_readable', 'code_type_readable']; 19 | 20 | const CARD_TYPE_MAP = [ 21 | 'GROUPON' => '团购券', 22 | 'DISCOUNT' => '折扣券', 23 | 'GIFT' => '礼品券', 24 | 'CASH' => '代金券', 25 | 'GENERAL_COUPON' => '通用券', 26 | 'MEMBER_CARD' => '会员卡', 27 | 'SCENIC_TICKET' => '景点门票', 28 | 'MOVIE_TICKET' => '电影票', 29 | 'BOARDING_PASS' => '飞机票', 30 | 'MEETING_TICKET' => '会议门票', 31 | 'BUS_TICKET' => '汽车票', 32 | ]; 33 | 34 | const CODE_TYPE_MAP = [ 35 | 'CODE_TYPE_TEXT' => '文本 ', 36 | 'CODE_TYPE_BARCODE' => '一维码 ', 37 | 'CODE_TYPE_QRCODE' => ' 二维码', 38 | 'CODE_TYPE_ONLY_QRCODE' => '二维码无code显示', 39 | 'CODE_TYPE_ONLY_BARCODE' => '一维码无code显示', 40 | ]; 41 | 42 | const DATE_INFO_TYPE_MAP = [ 43 | 'DATE_TYPE_FIX_TIME_RANGE' => '固定日期区间', 44 | 'DATE_TYPE_FIX_TERM' => '固定时长', 45 | 'DATE_TYPE_PERMANENT' => '永久有效', 46 | ]; 47 | 48 | public function getCardTypeReadableAttribute() 49 | { 50 | if ($cardType = $this->attributes['card_type']) { 51 | return self::CARD_TYPE_MAP[$cardType]; 52 | } 53 | } 54 | 55 | public function getCodeTypeReadableAttribute() 56 | { 57 | if ($cardType = $this->attributes['card_type']) { 58 | return self::CARD_TYPE_MAP[$cardType]; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Models/WechatConfig.php: -------------------------------------------------------------------------------- 1 | '公众号', 2 => '小程序'][$this->attributes['type']]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/WechatFake.php: -------------------------------------------------------------------------------- 1 | '普通商户号', 2 => '服务商'][$this->attributes['type']]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/WechatOrder.php: -------------------------------------------------------------------------------- 1 | attributes['gender'] ?? false) { 17 | return [0 => '未知', 1 => '男', 2 => '女'][$this->attributes['gender']]; 18 | } 19 | } 20 | 21 | /** 22 | * Get the identifier that will be stored in the subject claim of the JWT. 23 | * 24 | * @return mixed 25 | */ 26 | public function getJWTIdentifier() 27 | { 28 | return $this->getKey(); 29 | } 30 | 31 | /** 32 | * Return a key value array, containing any custom claims to be added to the JWT. 33 | * 34 | * @return array 35 | */ 36 | public function getJWTCustomClaims() 37 | { 38 | return []; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/ConfigService.php: -------------------------------------------------------------------------------- 1 | where('app_id', $appId)->firstOrFail(); 26 | 27 | $config = ['app_id' => $model->app_id, 'secret' => $model->secret, 'type' => $model->type]; 28 | 29 | Cache::forever('wechat.config.app_id.'.$model->app_id, $config); 30 | } 31 | 32 | return $this->getInstance($config); 33 | } 34 | 35 | /** 36 | * 获取实例 37 | * 38 | * @param array $config 39 | * @return \EasyWeChat\MiniProgram\Application|\EasyWeChat\OfficialAccount\Application 40 | */ 41 | protected function getInstance(array $config) 42 | { 43 | if ($config['type'] == 1) { 44 | return Factory::officialAccount([ 45 | 'app_id' => $config['app_id'], 46 | 'secret' => $config['secret'], 47 | ]); 48 | } else { 49 | return Factory::miniProgram([ 50 | 'app_id' => $config['app_id'], 51 | 'secret' => $config['secret'], 52 | ]); 53 | } 54 | } 55 | 56 | /** 57 | * 获取后台当前操作的实例 58 | * 59 | * @return \EasyWeChat\MiniProgram\Application|\EasyWeChat\OfficialAccount\Application 60 | */ 61 | public function getAdminCurrentApp() 62 | { 63 | $config = $this->getCurrent(); 64 | 65 | return $this->getInstanceByAppId($config->app_id); 66 | } 67 | 68 | /** 69 | * 获取后台当前操作的 WechatConfig 类 70 | * 71 | * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object|null 72 | */ 73 | public function getCurrent() 74 | { 75 | $key = config('admin.extensions.wechat.admin_current_key', 'wechat.admin.current'); 76 | 77 | $appId = Cache::get($key); 78 | 79 | if (!$appId) { 80 | $config = WechatConfig::query()->first(); 81 | 82 | if (!$config) { 83 | return null; 84 | } 85 | 86 | Cache::put($key,$config->app_id, Carbon::now()->addHours(2)); 87 | 88 | return $config; 89 | } 90 | 91 | return WechatConfig::query()->where('app_id', $appId)->first(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Services/MerchantService.php: -------------------------------------------------------------------------------- 1 | where('mch_id', $mchId)->firstOrFail(); 27 | 28 | $config = ['app_id' => $model->app_id, 'mch_id' => $model->mch_id, 'key' => $model->key, 'notify_url' => $model->notify_url]; 29 | 30 | Cache::forever('wechat.merchant.mch_id.'.$mchId, $config); 31 | } 32 | 33 | return Factory::payment([ 34 | 'app_id' => $config['app_id'], 35 | 'mch_id' => $config['mch_id'], 36 | 'key' => $config['key'], 37 | 'notify_url' => $config['notify_url'], 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/MiniService.php: -------------------------------------------------------------------------------- 1 | auth->session($code); 24 | 25 | Cache::forever($this->getSessionKey($result['openid']), $result['session_key']); 26 | 27 | return $result; 28 | } 29 | 30 | /** 31 | * 获取 session 的缓存 key 32 | * 33 | * @param string $openId 34 | * @return string 35 | */ 36 | protected function getSessionKey(string $openId) 37 | { 38 | return config('admin.extensions.wechat.session_key', 'mini.session.').$openId; 39 | } 40 | 41 | /** 42 | * 解密消息 43 | * 44 | * @param string $appId 45 | * @param string $openId 46 | * @param string $iv 47 | * @param string $encryptedData 48 | * @return mixed 49 | * @throws \EasyWeChat\Kernel\Exceptions\DecryptException 50 | */ 51 | public function decrypt(string $appId, string $openId, string $iv, string $encryptedData) 52 | { 53 | $app = \Hanson\LaravelAdminWechat\Facades\ConfigService::getInstanceByAppId($appId); 54 | 55 | $sessionKey = Cache::get($this->getSessionKey($openId)); 56 | 57 | return $app->encryptor->decryptData($sessionKey, $iv, $encryptedData); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Services/OrderService.php: -------------------------------------------------------------------------------- 1 | create($data); 17 | } 18 | 19 | /** 20 | * 统一下单 21 | * 22 | * @param string $mchId 23 | * @param string $tradeType 24 | * @param array $data 25 | * @return array 26 | * @throws PaymentException 27 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException 28 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException 29 | * @throws \GuzzleHttp\Exception\GuzzleException 30 | */ 31 | public function unify(string $mchId, string $tradeType, array $data) 32 | { 33 | $app = \Hanson\LaravelAdminWechat\Facades\MerchantService::getInstanceByMchId($mchId); 34 | 35 | $data = array_merge([ 36 | 'app_id' => $app->getConfig()['app_id'], 37 | 'mch_id' => $app->getConfig()['mch_id'], 38 | ], $data); 39 | 40 | $data['out_trade_no'] = $data['out_trade_no'] ?? $this->outTradeNo(); 41 | 42 | $result = $app->order->unify(array_merge($data, ['trade_type' => $tradeType])); 43 | 44 | if ($result['return_code'] === 'FAIL') { 45 | throw new PaymentException($result['return_msg']); 46 | } 47 | 48 | if ($result['result_code'] === 'FAIL') { 49 | throw new PaymentException($result['err_code_des']); 50 | } 51 | 52 | $order = $this->create($data); 53 | 54 | return ['unify' => $result, 'order' => $order]; 55 | } 56 | 57 | /** 58 | * 获取微信支付 js 参数 59 | * 60 | * @param string $mchId 61 | * @param string $tradeType 62 | * @param array $data 63 | * @return array 64 | * @throws PaymentException 65 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException 66 | * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException 67 | * @throws \GuzzleHttp\Exception\GuzzleException 68 | */ 69 | public function jsConfig(string $mchId, string $tradeType, array $data) 70 | { 71 | $app = \Hanson\LaravelAdminWechat\Facades\MerchantService::getInstanceByMchId($mchId); 72 | 73 | $result = $this->unify($mchId, $tradeType, $data); 74 | 75 | $result['config'] = $app->jssdk->bridgeConfig($result['unify']['prepay_id'], false); 76 | 77 | return $result; 78 | } 79 | 80 | protected function outTradeNo() 81 | { 82 | return date('YmdHis').(microtime(true) % 1) * 1000 .mt_rand(0, 9999); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Wechat.php: -------------------------------------------------------------------------------- 1 | commands($this->commands); 24 | } 25 | 26 | public function boot(Wechat $extension) 27 | { 28 | if (!Wechat::boot()) { 29 | return; 30 | } 31 | 32 | if ($views = $extension->views()) { 33 | $this->loadViewsFrom($views, 'wechat'); 34 | } 35 | 36 | if (file_exists($routes = base_path('routes/wechat_admin.php'))) { 37 | $this->loadRoutesFrom($routes); 38 | } 39 | if (file_exists($routes = base_path('routes/wechat_api.php'))) { 40 | $this->loadRoutesFrom($routes); 41 | } 42 | 43 | Admin::booting(function () { 44 | Form::extend('wechatMenu', MenuFormField::class); 45 | }); 46 | 47 | if ($this->app->runningInConsole()) { 48 | $this->publishes([ 49 | __DIR__.'/../database' => database_path(), 50 | __DIR__.'/../routes' => base_path('routes'), 51 | $extension->assets() => public_path('vendor/laravel-admin-ext/wechat'), 52 | ],'laravel-admin-wechat'); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | json(['error_code' => 0, 'data' => $data]); 10 | } 11 | } 12 | 13 | /** 14 | * api 错误 json 15 | */ 16 | if (!function_exists('fail')) { 17 | function fail($errorMsg, $errorCode = -1) 18 | { 19 | return response()->json(['error_code' => $errorCode, 'error_message' => $errorMsg]); 20 | } 21 | } 22 | --------------------------------------------------------------------------------