Browse Source

整理异步任务系统,对应的,整理event系统

joe 4 years ago
parent
commit
2de0fd5fc3

+ 2 - 1
app/admin/controller/Test.php

@@ -91,7 +91,7 @@ class Test
 
     protected function test_async_func()
     {
-        AsyncClass::push('\app\admin\controller\Test', [], 'gd', []);
+        return AsyncClass::push('tw\async\tasks\UserTaskClass', [], 'generate_user_poster', [3]);
     }
 
 
@@ -111,6 +111,7 @@ class Test
 
     public function test()
     {
+        // echo $this->test_async_func();
         // $this->test_redis();
         // echo UserSearch::InsertHistory(1, "good");
         // print_r(UserSearch::getList(1, 20));

+ 1 - 0
app/api/controller/store/StoreProductController.php

@@ -160,6 +160,7 @@ class StoreProductController
                 if ($status) {
                     User::where('uid', $uid)->update(['is_promoter' => 1]);
                     $user->is_promoter = 1;
+                    event('UserBecomedPromoter', [$user->toArray()]);
                 }
             }
             if ($user->is_promoter) {

+ 1 - 0
app/api/controller/user/UserController.php

@@ -109,6 +109,7 @@ class UserController
             if ($status) {
                 User::where('uid', $user['uid'])->update(['is_promoter' => 1]);
                 $user['is_promoter'] = 1;
+                event('UserBecomedPromoter', [$user]);
             } else {
                 $storeBrokeragePrice = sys_config('store_brokerage_price', 0);  // 分銷滿足金額
                 $user['promoter_price'] = bcsub($storeBrokeragePrice, $price, 2);

+ 14 - 1
app/common.php

@@ -34,8 +34,15 @@ define('SHIPPING_SELF_COLLECT', 2);
 define('DISTRIBUTE_SPECIFIED', 1);  // 指定分銷
 define('DISTRIBUTE_EVERYONE', 2);   // 人人分銷
 
-// 
+// 文件上传类型
+define('UPLOAD_FS', 1);
+define('UPLOAD_QINIU', 2);
+define('UPLOAD_ALI_OSS', 3);
+define('UPLOAD_TENCENT_COS', 4);
+
+// 每天秒数
 define('SECONDS_OF_ONEDAY', 86400);
+define('DS', DIRECTORY_SEPARATOR);
 
 /**
  * 获取所有 幸运2021 活动及其子活动 ID 列表
@@ -603,4 +610,10 @@ if (!function_exists('mapped_implode')) {
                 )
             );
     }
+}
+
+if (!function_exists('async_call')) {
+    function async_call(string $className, array $classArgs, string $methodName, array $methodArgs) {
+        
+    }
 }

+ 8 - 2
app/models/user/User.php

@@ -112,8 +112,9 @@ class User extends BaseModel
         } else {
             $becomingPromoter = false;
         }
+        $ret = '';
         if ($userInfo->spread_uid) {
-            return self::edit([
+            $ret = self::edit([
                 'nickname' => $wechatUser['nickname'] ?: '',
                 'avatar' => $wechatUser['headimgurl'] ?: '',
                 'is_promoter' => $becomingPromoter ? 1 : $userInfo->is_promoter,
@@ -149,8 +150,13 @@ class User extends BaseModel
                 $data['spread_uid'] = $wechatUser['code'];
                 $data['spread_time'] = time();
             }
-            return self::edit($data, $uid, 'uid');
+            $ret = self::edit($data, $uid, 'uid');
         }
+        if ($becomingPromoter) {
+            $userInfo->is_promoter = 1;
+            event('UserBecomedPromoter', [$userInfo->toArray()]);
+        }
+        return $ret;
     }
 
     /**

+ 7 - 4
crmeb/command/Task.php

@@ -88,23 +88,26 @@ class Task extends Command
 
                 foreach($asyncTasks as $t) {
                     if ($t->getCmd() == $payload['cmd']) {
-                        if (!$t->exec($payload)) {
+                        $retval = $t->exec($payload);
+                        if (!$retval) {
                             Log::warning('async task executed return false:' . $str_payload);
                         }
+                        Log::info("task: $str_payload return:". json_encode($retval));
                         $executed = true;
                     }
                 }
                 if (!$executed) {
                     Log::error('task unsupported cmd:' . $str_payload);
-                    Beanstalk::release($job, 1024, 30);
+                    // Beanstalk::release($job, 1024, 30);
+                    Beanstalk::delete($job);
                     continue;
                 }
 
                 Beanstalk::delete($job);
             } catch (\Exception $e) {
                 Log::error('task exception:' . $e->getMessage() . ' data:' . $job->getData());
-                Beanstalk::release($job, 1024, 30); 
-                // Beanstalk::delete($job); // testing
+                // Beanstalk::release($job, 1024, 30); 
+                Beanstalk::delete($job); // testing
             }
         } // while
     }

+ 6 - 1
crmeb/subscribes/OrderSubscribe.php

@@ -140,14 +140,19 @@ class OrderSubscribe
                 event('UserFirstOrder', ['order' => $order]);
             }
             $userInfo->pay_count = $userInfo->pay_count + 1;
+            $becomingPromoter = false; // flag
             if (!$userInfo->is_promoter) {
-                $price = StoreOrder::where(['paid' => 1, 'refund_status' => 0, 'uid' => $userInfo['uid']])->sum('pay_price');
+                $price = StoreOrder::where(['paid' => 1, 'refund_status' => 0, 'uid' => $userInfo->uid])->sum('pay_price');
                 $status = is_brokerage_statu($price);
                 if ($status) {
+                    $becomingPromoter = true;
                     $userInfo->is_promoter = 1;
                 }
             }
             $userInfo->save();
+            if ($becomingPromoter) {
+                event('UserBecomedPromoter', [$userInfo->toArray()]);
+            }
         }
 
         //发送模版消息、客服消息、短信、小票打印给客户和管理员

+ 13 - 0
crmeb/subscribes/UserSubscribe.php

@@ -9,6 +9,7 @@ use app\admin\model\system\SystemAttachment;
 use app\admin\model\user\UserBill;
 use app\models\user\UserLevel;
 use app\models\user\UserNotice;
+use crmeb\services\async\task\AsyncClass;
 use think\facade\Log;
 use think\facade\Config;
 
@@ -104,6 +105,8 @@ class UserSubscribe
         $request = app('request');
         User::edit(['last_time' => time(), 'last_ip' => $request->ip()], $userInfo->uid, 'uid');
 
+        // 对现存用户处理,用于过渡
+        AsyncClass::push('tw\async\tasks\UserTaskClass', [], 'generate_user_poster', [$userInfo->uid]);
         Log::debug('EVENT user ' . $userInfo->uid . ' login.');
     }
 
@@ -122,6 +125,16 @@ class UserSubscribe
 
         }
     }
+
+    /**
+     * 用户成为推广员 UserBecomedPromoter
+     */
+    public function onUserBecomedPromoter($event)
+    {
+        list($user) = $event;
+        AsyncClass::push('tw\async\tasks\UserTaskClass', [], 'generate_user_poster', [$user['uid']]);
+        Log::debug('EVENT user' . $user['uid'] . ' has becomed promoter');
+    }
     
     /**
      * 用户升级成功 UserLevelUp

+ 40 - 0
docs/config/nginx.conf

@@ -0,0 +1,40 @@
+server {
+	server_name twongd.shotshock.shop;
+    root /var/www/twong/public;
+	index index.php index.html;
+
+    location ^~ /mobilex/ {
+		alias /var/www/twong/public/mobilex/;
+	}
+
+    location / {
+		if (!-e $request_filename) {
+			rewrite ^(.*)$ /index.php?s=$1 last;
+			break;
+		}
+	}
+
+	location ~ \.php {
+		include fastcgi.conf;
+		fastcgi_pass unix:/run/php/php7.2-fpm.sock;
+	}
+
+    listen 443 ssl; # managed by Certbot
+    ssl_certificate /etc/letsencrypt/live/twong.shotshock.shop/fullchain.pem; # managed by Certbot
+    ssl_certificate_key /etc/letsencrypt/live/twong.shotshock.shop/privkey.pem; # managed by Certbot
+    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
+    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
+
+}
+
+# 转向 https
+server {
+    if ($host = twongd.shotshock.shop) {
+        return 301 https://$host$request_uri;
+    } # managed by Certbot
+
+	listen 80;
+	
+	server_name twongd.shotshock.shop;
+    return 404; # managed by Certbot
+}

+ 29 - 0
docs/config/supervisor.conf

@@ -0,0 +1,29 @@
+[program:beanstalkd]
+command=beanstalkd -b /var/log/beanstalkd -F -l 127.0.0.1 -p 11300 -u root
+directory=/var/log/beanstalkd
+user=root
+startsecs=5
+redirect_stderr=true
+stdout_logfile_maxbytes=20MB
+stdout_logfile_backups=10
+stdout_logfile=/tmp/beanstalkd.log
+
+[program:timer]
+command=php think timer start
+directory=/opt/www/twong
+user=www-data
+startsecs=6
+redirect_stderr=true
+stdout_logfile_maxbytes=20MB
+stdout_logfile_backups=10
+stdout_logfile=/tmp/think_timer.log
+
+[program:workerman]
+command=php think workerman start
+directory=/opt/www/twong
+user=www-data
+startsecs=6
+redirect_stderr=true
+stdout_logfile_maxbytes=20MB
+stdout_logfile_backups=10
+stdout_logfile=/tmp/think_workerman.log

+ 40 - 0
docs/可维护性.md

@@ -0,0 +1,40 @@
+## 命名
+
+为避免混淆,代码中有以下命名约定。
+
+1. 原则
+
+变量/函数名/类名的定义采用概念的逆向单词排列。比如: QQ三方认证登录,微信三方认证登录分别对应为
+
+
+QQ三方认证登录 : auth_qq
+
+微信三方认证登录: auth_wechat
+
+而不是 qq_auth, wechat_auth
+
+2. 名词对应表
+
+支付宝 alipay
+微信支付 wechatpay
+公众号  public
+微信公众号 wechat_public
+小程序 mp
+微信小程序 wechat_mp
+app: app
+android app: android
+ios app: ios
+
+## thinkphp model
+
+在 controller 中调用 model 尽量封装到 model 类内部,避免 XxxxModel::where()->select(); 这种就地使用的方式。
+
+## composer 中添加 autoload 目录
+
+1. 在 composer.json 中添加
+2. 执行 composer dumpautoload
+
+## 测试
+
+1. tests 目录中添加 TestCase
+2. 执行 ./vendor/bin/phpunit tests/*

+ 27 - 0
tests/UserTaskTest.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace tests;
+require __DIR__ . '/../vendor/autoload.php';
+
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+use PHPUnit\Framework\TestCase;
+use app\models\user\User;
+use crmeb\command\Task;
+use crmeb\services\async\task\AsyncClass;
+use tw\async\tasks\UserTaskClass;
+
+class UserTaskTest extends TestCase
+{
+    public function test_generate_user_poster()
+    {
+        $app = new \think\App();
+        $app->http->run();
+        $r = AsyncClass::push('tw\async\tasks\UserTaskClass', [], 'generate_user_poster', [3]);
+        // $user = User::getUserInfo(3);
+        // list($suc, $data) = UserTaskClass::generate_user_poster($user);
+        // self::assertTrue($suc);
+        echo $r;
+        self::assertTrue($r);
+    }
+}

+ 4 - 1
think

@@ -16,4 +16,7 @@ namespace think;
 require __DIR__ . '/vendor/autoload.php';
 
 // 应用初始化
-(new App())->console->run();
+$app = new App();
+$app->http->run();
+$app->console->run();
+// (new App())->console->run();

+ 5 - 0
tw/async/README.md

@@ -0,0 +1,5 @@
+tw\async
+
+是 crmeb\async 模块的映射,crmeb\services\sync 早晚要合并到这个空间下。
+
+这个模块主要和异步任务有关,通过 beanstalk 来异步执行一些代码。

+ 228 - 0
tw/async/tasks/UserTaskClass.php

@@ -0,0 +1,228 @@
+<?php 
+namespace tw\async\tasks;
+
+use app\admin\model\system\SystemAttachment;
+use app\models\routine\RoutineCode;
+use crmeb\services\upload\Upload;
+use app\models\routine\RoutineQrcode;
+use crmeb\services\UtilService;
+use think\facade\Log;
+use app\models\user\User;
+
+/**
+ * 和用户相关的异步任务,采用 AsyncClass 的执行方法 
+ */
+class UserTaskClass {
+    const QINIU_PATH = 'user_share_qrcode'; // 海报保存在七牛目录的子路径
+
+
+    /**
+     * 生成用户分享海报
+     * @param $uid
+     * @param $postId : 数据库配置的海报背景 ID
+     * @param $is_promoter
+     * @return string
+     */
+    public static function generate_user_poster_filename($uid, $posterId, $is_promoter=true) : string
+    {
+        return $uid . '_' . $is_promoter . '_' . $posterId . '_user_routine_poster.jpg';
+    }
+
+    /**
+     * 生成用户分享二维码文件名
+     * @param $uid
+     * @param $is_promoter
+     * @return string
+     */
+    public static function generate_user_share_qrcode_filename($uid, $is_promoter=true) : string
+    {
+        return $uid . '_' . $is_promoter . '_user_routine.jpg';
+    }
+
+    /**
+     * 生成用户海报
+     * @param $user
+     * @param $siteSsl
+     * @param $type
+     * @return array: [bool, str] : 表示是否成功,错误描述
+     */
+    public static function generate_user_poster(int $uid, bool $siteSsl=true, int $type=1) : array
+    {
+        $rootPath = app()->getRootPath();
+        try {
+            $resRoutine = true;//小程序
+            $resWap = true;//公众号
+            $siteUrl = sys_config('site_url');
+            // 配置的海报背景模板
+            $routineSpreadBanner = sys_data('routine_spread_banner');
+            if (!count($routineSpreadBanner)) {
+                $err = '暂无海报';
+                Log::error($err);
+                return [false, $err];
+            }
+            $user = User::getUserInfo($uid);
+            if (!$user) {
+                $err = '用户不存在';
+                Log::error($err);
+                return [false, $err];
+            }
+            $is_promoter = $user['is_promoter'] ?? 0;
+            if (!$is_promoter) {
+                return [false, '没有推广权限'];
+            }
+
+            if ($type == 1) {
+                // 直接获取用户海报
+                foreach ($routineSpreadBanner as &$item) {
+                    $item['namep'] = self::generate_user_poster_filename($user['uid'], $item['id'], $user['is_promoter']);
+                }
+                $posters = SystemAttachment::whereIn('name', array_column($routineSpreadBanner, 'namep'))->field('name, att_dir')->select()->toArray();
+                if (count($posters)) {
+                    foreach ($routineSpreadBanner as $key => &$item) {
+                        foreach ($posters as $poster) {
+                            if ($item['namep'] == $poster['name']) {
+                                $item['poster'] = $poster['att_dir'];
+                            }
+                        }
+                        unset($item['namep']);
+                        if (!isset($item['poster'])) {
+                            unset($routineSpreadBanner[$key]);
+                        }
+                    }
+                    $routineSpreadBanner = array_values($routineSpreadBanner);
+                    if (count($routineSpreadBanner) > 0) {
+                        return [true, $routineSpreadBanner];
+                    }
+                }
+
+                //小程序个人分享二维码文件名
+                $name = self::generate_user_share_qrcode_filename($user['uid'], $user['is_promoter']);
+                $imageInfo = SystemAttachment::getInfo($name, 'name');
+                //检测远程文件是否存在
+                // 20210722 注释掉,太卡
+                if (isset($imageInfo['att_dir']) && 
+                    strstr($imageInfo['att_dir'], 'http') !== false && 
+                    curl_file_exist($imageInfo['att_dir']) === false) {
+                    $imageInfo = null;
+                    SystemAttachment::where(['name' => $name])->delete();
+                }
+                if (!$imageInfo) { // 数据库中无记录
+                    $res = RoutineCode::getShareCode($user['uid'], 'spread', '', '');
+                    if (!$res) {
+                        return [false, '二维码生成失败'];
+                    }
+                    $uploadType = (int)sys_config('upload_type', UPLOAD_FS);
+                    $upload = new Upload($uploadType, [
+                        'accessKey' => sys_config('accessKey'),
+                        'secretKey' => sys_config('secretKey'),
+                        'uploadUrl' => sys_config('uploadUrl'),
+                        'storageName' => sys_config('storage_name'),
+                        'storageRegion' => sys_config('storage_region'),
+                    ]);
+                    // 上传分享二维码 [ qiniu 添加一个子路径]
+                    $remoteName = $uploadType == UPLOAD_QINIU ?  self::QINIU_PATH . '/' . $name : $name;
+                    $uploadRes = $upload->to('routine/spread/code')->validate()->stream($res['res'], $remoteName);
+                    if ($uploadRes === false) {
+                        $err = $upload->getError();
+                        Log::error("upload error:" . $err);
+                        return [false, $err];
+                    }
+                    $upload->delete($name);
+                    $imageInfo = $upload->getUploadInfo();
+                    // 20210919 为了能小程序下载,这个函数上传的图片全部使用 https
+                    $httpsUrl = set_http_type($imageInfo['dir']);
+                    $imageInfo['image_type'] = $uploadType;
+                    SystemAttachment::attachmentAdd(basename($imageInfo['name']), $imageInfo['size'], $imageInfo['type'], $httpsUrl, $httpsUrl, 1, $imageInfo['image_type'], $imageInfo['time'], 2);
+                    RoutineQrcode::setRoutineQrcodeFind($res['id'], ['status' => 1, 'url_time' => time(), 'qrcode_url' => $imageInfo['dir']]);
+                    $urlCode = $imageInfo['dir'];
+                } else {
+                    $urlCode = $imageInfo['att_dir'];
+                }
+
+                if ($imageInfo['image_type'] == UPLOAD_FS) {
+                    $urlCode = $siteUrl . $urlCode;
+                }
+                $siteUrlHttps = set_http_type($siteUrl, $siteSsl ? 0 : 1);
+                $filelink = [
+                    'Bold' => 'public/static' . DS . 'font' . DS . 'Alibaba-PuHuiTi-Regular.otf',
+                    'Normal' => 'public/static' . DS . 'font' . DS . 'Alibaba-PuHuiTi-Regular.otf',
+                ];
+                if (!file_exists($filelink['Bold'])) {
+                    $err = '缺少字体文件Bold';
+                    Log::error($err);
+                    return [false, $err];
+                }
+                if (!file_exists($filelink['Normal'])) {
+                    $err = '缺少字体文件Normal';
+                    Log::error($err);
+                    return [false, $err];
+                }
+                foreach ($routineSpreadBanner as $key => &$item) {
+                    $posterInfo = '海报生成失败';
+                    $config = array(
+                        'image' => array(
+                            array(
+                                'url' => $urlCode,     //二维码资源
+                                'stream' => 0,
+                                'left' => 114,
+                                'top' => 790,
+                                'right' => 0,
+                                'bottom' => 0,
+                                'width' => 120,
+                                'height' => 120,
+                                'opacity' => 100
+                            )
+                        ),
+                        'text' => array(
+                            array(
+                                'text' => $user['nickname'],
+                                'left' => 250,
+                                'top' => 840,
+                                'fontPath' => $rootPath . $filelink['Bold'],     //字体文件
+                                'fontSize' => 16,             //字号
+                                'fontColor' => '40,40,40',       //字体颜色
+                                'angle' => 0,
+                            ),
+                            array(
+                                'text' => '邀请您加入' . sys_config('site_name'),
+                                'left' => 250,
+                                'top' => 880,
+                                'fontPath' => $rootPath . $filelink['Normal'],     //字体文件
+                                'fontSize' => 16,             //字号
+                                'fontColor' => '40,40,40',       //字体颜色
+                                'angle' => 0,
+                            )
+                        ),
+                        'background' => $item['pic'],
+                        'name' => self::QINIU_PATH . '/' . self::generate_user_poster_filename($user['uid'],  $item['id'], $user['is_promoter']),
+                    );
+                    $resRoutine = $resRoutine && $posterInfo = UtilService::setSharePoster($config, 'routine/spread/poster');
+                    if (!is_array($posterInfo)) {
+                        Log::error($posterInfo);
+                        return [false, $posterInfo];
+                    }
+                    // 20210919 为了能小程序下载,这个函数上传的图片全部使用 https
+                    $httpsUrl = set_http_type($posterInfo['dir']);
+                    SystemAttachment::attachmentAdd(basename($posterInfo['name']), $posterInfo['size'], $posterInfo['type'], $httpsUrl, $httpsUrl, 1, $posterInfo['image_type'], $posterInfo['time'], 2);
+                    if ($resRoutine) {
+                        if ($posterInfo['image_type'] == UPLOAD_FS)
+                            $item['poster'] = $siteUrlHttps . $posterInfo['dir'];
+                        else
+                            $item['poster'] = set_http_type($posterInfo['dir'], $siteSsl ? 0 : 1);
+                        $item['poster'] = str_replace('\\', '/', $item['poster']);
+                    }
+                }
+            } else if ($type == 2) {
+                Log::error(__FUNCTION__ . ' bad parameter type=2');
+            }
+            if ($resRoutine && $resWap) {
+                return [true, $routineSpreadBanner];
+            } else {
+                return [false, '生成图片失败'];
+            }
+        } catch (\Exception $e) {
+            Log::error('生成图片时,系统错误:' . $e->getMessage());
+            return [false, '生成图片时,系统错误:' . $e->getMessage()];
+        }
+    }
+}

+ 0 - 0
tw/services/Alipay.php


+ 24 - 0
tw/services/ThirdPartyLogin.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace tw\services;
+
+/**
+ * 三方登录封装,主要基于 Yurun-oauth-login
+ * 
+ * crmeb 的 WechatService 封装了 overtrue 的实现,不支持获取 unionId。
+ * 
+ * overture 是一个只针对微信 API 的 SDK 库
+ */
+class ThirdPartyLogin
+{
+
+    public static function auth_app_wx()
+    {
+
+    }
+
+    public static function auth_mp_wx()
+    {
+
+    }
+}

+ 0 - 0
tw/services/WechatPay.php