KeepContinueReadV3.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: z-yang
  5. * Date: 2020/6/8
  6. * Time: 17:41
  7. */
  8. namespace App\Console\Commands\SmartPush;
  9. use Illuminate\Console\Command;
  10. use DB;
  11. use Redis;
  12. use GuzzleHttp\Client;
  13. use App\Http\Controllers\WechatController;
  14. use GuzzleHttp\Psr7\Request as GuzzleRequest;
  15. use GuzzleHttp\Pool;
  16. use Hashids;
  17. use Log;
  18. /*
  19. 继续阅读智能推送时间逻辑:
  20. 遍历阅读后离开阅读器的用户,若3小时后未再次访问阅读器,需推送一条继续阅读智能推送消息
  21. 若再过5小时后扔未访问,需推送第二条继续阅读智能推送消息
  22. 若再过5小时后扔未访问,需推送第三条继续阅读智能推送消息
  23. 若再过12小时后扔未访问,需推送第四条继续阅读智能推送消息
  24. 若再过12小时后扔未访问,需推送第五条继续阅读智能推送消息
  25. 期间任何一个时间节点遍历时若被打断,则重新开始计算
  26. */
  27. class KeepContinueReadV3 extends Command
  28. {
  29. /**
  30. * The name and signature of the console command.
  31. *
  32. * @var string
  33. */
  34. protected $signature = 'SmartPush:KeepContinueReadV3';
  35. /**
  36. * The console command description.
  37. *
  38. * @var string
  39. */
  40. protected $description = '持续阅读推送';
  41. private static $role = [0,3,5,5,12,12];
  42. /**
  43. * Create a new command instance.
  44. *
  45. * @return void
  46. */
  47. public function __construct()
  48. {
  49. parent::__construct();
  50. }
  51. /**
  52. * Execute the console command.
  53. *
  54. * @return mixed
  55. */
  56. public function handle()
  57. {
  58. $str = str_random(32);
  59. Log::info('KeepContinueReadV3 start at :'.date('Y-m-d H:i:s').',flag is :'.$str);
  60. $this->send();
  61. Log::info('KeepContinueReadV3 end at :'.date('Y-m-d H:i:s').',flag is :'.$str);
  62. }
  63. private function send(){
  64. $client = new Client();
  65. $channel_id = $this->getAllSite();
  66. if(!$channel_id) return ;
  67. $requests = $this->start($channel_id);
  68. $pool = new Pool($client, $requests, [
  69. 'concurrency' => 5,
  70. 'fulfilled' => function ($response, $index) {
  71. },
  72. 'rejected' => function ($reason, $index) {
  73. },
  74. ]);
  75. $promise = $pool->promise();
  76. $promise->wait();
  77. }
  78. private function getAllSite(){
  79. $info = DB::connection('api_mysql')->table('custom_msg_switchs')->where('custom_category','continue_read')->first();
  80. if(!$info) return [];
  81. $default_status = $info->default_switch_status;
  82. $site_list = explode(',',redisEnv('INNER_SITE'));
  83. $on_distribution_channel_id = [];
  84. foreach ($site_list as $distribution_channel_id){
  85. $switch_info = DB::connection('api_mysql')->table('custom_msg_switchs_msgs')
  86. ->where('distribution_channel_id',$distribution_channel_id)
  87. ->where('custom_category','continue_read')
  88. ->select('status')
  89. ->first();
  90. if($switch_info){
  91. $switch = $switch_info->status;
  92. }else{
  93. $switch = $default_status;
  94. }
  95. if($switch != 1) continue;
  96. $on_distribution_channel_id[] = $distribution_channel_id;
  97. }
  98. return $on_distribution_channel_id;
  99. }
  100. private function start($distribution_channel_ids){
  101. $temp = 0;
  102. //$sites = $this->channelIds();
  103. while (true){
  104. $user = DB::connection('api_mysql')->table('temp_force_subscribe_users')
  105. ->select('id','uid','distribution_channel_id','openid','appid','last_interactive_time')
  106. ->where('id','>',$temp)
  107. ->whereIn('distribution_channel_id',$distribution_channel_ids)
  108. ->orderBy('id')
  109. ->limit(1000)
  110. ->get();
  111. if(!$user) break;
  112. foreach ($user as $item){
  113. $temp = $item->id;
  114. if( (time() - strtotime($item->last_interactive_time)) > 2*86400 ) continue;
  115. //if(!in_array($item->distribution_channel_id,$sites)) continue;
  116. $read_info = $this->getReadRecord($item->uid);
  117. if(empty($read_info['first']) || !isset($read_info['time']) || empty($read_info['time'])){
  118. continue;
  119. }
  120. $is_access = $this->isAccess($item->uid,$read_info['time'],$now_role,$is_first);
  121. if(!$is_access) continue;
  122. $access_token = $this->getAccessToken($item->appid);
  123. if(!$access_token)continue;
  124. $url = 'https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token='.$access_token;
  125. $content = $this->content($item->uid,$item->distribution_channel_id,$read_info);
  126. $request = new GuzzleRequest('post',$url,[],\GuzzleHttp\json_encode([
  127. 'touser'=>$item->openid,
  128. 'msgtype'=>'text',
  129. 'text'=>['content'=>$content]
  130. ],JSON_UNESCAPED_UNICODE));
  131. if($is_first){
  132. DB::connection('api_mysql')->table('custom_push_keep_continue_v2')->where('uid',$item->uid)->update([
  133. 'times'=>$now_role,
  134. 'updated_at'=>date('Y-m-d H:i:s')
  135. ]);
  136. }else{
  137. DB::connection('api_mysql')->table('custom_push_keep_continue_v2')->insert([
  138. 'uid'=>$item->uid,
  139. 'times'=>$now_role,
  140. 'created_at'=>date('Y-m-d H:i:s'),
  141. 'updated_at'=>date('Y-m-d H:i:s')
  142. ]);
  143. }
  144. yield $request;
  145. }
  146. }
  147. }
  148. private function isAccess($uid,$last_read_time,&$now_role,&$is_first){
  149. //获取用户上次发送的规则
  150. $prev_send_info = DB::connection('api_mysql')->table('custom_push_keep_continue_v2')->where('uid',$uid)->select('updated_at','times')->orderBy('id','desc')->first();
  151. //上次发送规则
  152. $last_role = 0;
  153. $is_first = false;
  154. $last_send_time = 0;
  155. if($prev_send_info){
  156. $is_first = true;
  157. $last_role = $prev_send_info->times;
  158. $last_send_time = strtotime($prev_send_info->updated_at);
  159. }
  160. $now_role = $last_role+1;
  161. if($now_role == 6) $now_role = 1;
  162. //上次更新时间,也就是上次发送时间
  163. //第一次发送是根据用户离开阅读器的时间,即当前时间和用户上次阅读的时间比较是否查过规定的时间
  164. if($now_role == 1){
  165. if((time() - $last_read_time) < self::$role[$now_role] * 3600){
  166. //小于发送时间
  167. return false;
  168. }
  169. return true;
  170. }
  171. //是否到达检测时间,即上次发送时间加上当前规则所规定的时间,差距在半个小时以内就算到达
  172. if(abs( time()-($last_send_time+self::$role[$now_role] * 3600)) > 1800){
  173. //没有到达时间
  174. return false;
  175. }
  176. //用户阅读时间是否符合条件
  177. $time_diff = 0;
  178. foreach (self::$role as $k=>$t){
  179. if($k <= $now_role){
  180. $time_diff += $t;
  181. }
  182. }
  183. if( (time() - $last_read_time) >= $time_diff*3600){
  184. //到了时间还是没阅读,就发送
  185. return true;
  186. }else{
  187. //时间节点遍历时若被打断,则重新开始计算
  188. DB::connection('api_mysql')->table('custom_push_keep_continue_v2')->where('uid',$uid)->update([
  189. 'times'=>0,
  190. 'updated_at'=>date('Y-m-d H:i:s')
  191. ]);
  192. return false;
  193. }
  194. }
  195. private function content($uid,$distribution_channel_id,$read_info){
  196. $domain = sprintf('https://site%s.%s.com',encodeDistributionChannelId($distribution_channel_id),
  197. env('CUSTOM_HOST'));
  198. $user_info = DB::connection('api_mysql')->table('users')->where('id',$uid)->select('nickname')->first();
  199. $nickname = '读者';
  200. if($user_info && $user_info->nickname)$nickname = $user_info->nickname;
  201. $content_format = "@%s 为您推荐上次未看完的小说\r\n\r\n点击<a href='%s'>继续阅读</a>❤\r\n\r\n";
  202. $content = sprintf($content_format,$nickname,$domain.$read_info['first']['url']);
  203. if(!empty($read_info['seconds'])){
  204. $content .= "历史阅读记录:\r\n\r\n";
  205. foreach ($read_info['seconds'] as $record_item){
  206. $content .= sprintf(" 🌳 <a href='%s'>%s</a>\r\n",$domain.$record_item['url'],$record_item['book_name']);
  207. }
  208. }
  209. $content .= "\r\n为了方便下次阅读,请<a href='http://cdn-pro.18yuedu.com/h5/top.html'>置顶公众号</a>";
  210. return $content;
  211. }
  212. private function getAccessToken($appid){
  213. try{
  214. $WechatController = new WechatController($appid);
  215. $accessToken = $WechatController->app->access_token; // EasyWeChat\Core\AccessToken 实例
  216. $token = $accessToken->getToken(); // token 字符串
  217. return $token;
  218. }catch(\Exception $e){
  219. \Log::error($e->getMessage());
  220. }
  221. return '';
  222. }
  223. private function getReadRecord($uid){
  224. $records = Redis::hgetall('book_read:' . $uid);
  225. $data = ['first'=>[],'seconds'=>[],'time'=>''];
  226. foreach ($records as $k=>$item){
  227. if($k == 'last_read'){
  228. $record_arr = explode('_',$item);
  229. $bid = $record_arr[0];
  230. //$book_name = $this->bid2BookName($bid);
  231. $bid = Hashids::encode($bid);
  232. $cid = $record_arr[1];
  233. $time = $record_arr[2];
  234. $data['first'] = [
  235. 'url' => '/reader?bid='.$bid.'&cid='.$cid.'&fromtype=continue_read_v2',
  236. 'book_name'=>'',
  237. ];
  238. $data['time'] = $time;
  239. continue;
  240. }
  241. if(!is_numeric($k)) continue;
  242. $record = explode('_', $item);
  243. $latest_read_cid = $record[0];
  244. $book_name = self::bid2BookName($k);
  245. $latest_read_time = $record[count($record) - 1];
  246. $data['seconds'][] =[
  247. 'url' => '/reader?bid='.Hashids::encode($k).'&cid='.$latest_read_cid.'&fromtype=continue_read_v2',
  248. 'book_name'=>$book_name,
  249. 'time'=>$latest_read_time
  250. ];
  251. }
  252. $temp = $data['seconds'];
  253. if($temp){
  254. usort($temp, function ($a, $b) {
  255. if ($a['time'] >= $b['time']) return -1;
  256. return 1;
  257. });
  258. }
  259. $temp_res = [];
  260. foreach ($temp as $k=>$it){
  261. $temp_res[] = $it;
  262. if($k>=2) break;
  263. }
  264. $data['seconds'] = $temp_res;
  265. return $data;
  266. }
  267. private function bid2BookName($bid){
  268. $book_name = null;
  269. if(is_null($book_name)){
  270. $book_key = 'wap:string:book:'.$bid;
  271. $book_name = Redis::get($book_key);
  272. Redis::EXPIRE($book_key,3600);
  273. if(!$book_name){
  274. $book_name = '';
  275. $book_info = DB::connection('api_mysql')->table('book_configs')->where('bid',$bid)->select('book_name')->first();
  276. //$book_info = BookConfigService::getBookById($bid);
  277. if($book_info && isset($book_info->book_name)){
  278. $book_name = $book_info->book_name;
  279. Redis::setex($book_key,3600,$book_name);
  280. }
  281. }
  282. }
  283. return $book_name;
  284. }
  285. private function channelIds(){
  286. $str = '5,123,14,13,8';
  287. return explode(',',$str);
  288. }
  289. }