小说推广数据系统

NovelDataStatistics.php 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: shensong
  5. * Date: 2021/6/3
  6. * Time: 18:17
  7. */
  8. namespace App\Console\Commands;
  9. use App\Models\Account;
  10. use App\Models\AdsReport;
  11. use App\Models\conf\AccountConf;
  12. use App\Models\conf\ADQAccountBinding;
  13. use App\Models\conf\NovelConf;
  14. use App\Models\NovelData;
  15. use App\Models\Order;
  16. use App\Services\WxAccountService;
  17. use App\RedisModel;
  18. use Illuminate\Console\Command;
  19. class NovelDataStatistics extends Command
  20. {
  21. protected $signature = 'NovelDataStatistics {param}';
  22. protected $description = '小说数据趋势每日离线统计';
  23. protected $redisKey = 'novel_promote:novel_data_statistics';
  24. /**
  25. * Create a new command instance.
  26. *
  27. * @return void
  28. */
  29. public function __construct()
  30. {
  31. \DB::disableQueryLog();
  32. set_time_limit(0);
  33. ini_set('memory_limit', -1);
  34. parent::__construct();
  35. }
  36. public function handle()
  37. {
  38. $this->infoMessage(date('Y-m-d H:i:s') . ' 开始执行~');
  39. $param = $this->argument('param');
  40. if(empty($param) || !in_array($param, [1,2])) {
  41. $this->infoMessage('执行参数错误');
  42. }
  43. switch($param) {
  44. case 1:
  45. // 生产
  46. $this->produce();
  47. break;
  48. case 2:
  49. // 消费
  50. $this->consume();
  51. break;
  52. }
  53. $this->infoMessage(date('Y-m-d H:i:s') . ' 结束执行~');
  54. return ;
  55. }
  56. public function produce()
  57. {
  58. $accountConf = AccountConf::query()
  59. ->where('enable', 1)
  60. ->get();
  61. if(empty($accountConf)) {
  62. $this->infoMessage(' 暂无小说配置数据');
  63. die;
  64. }
  65. $novelDataList = NovelConf::query()
  66. ->where('enable', 1)
  67. // ->where('id', 108)
  68. ->get();
  69. if(empty($novelDataList)) {
  70. $this->infoMessage(' 暂无启用的小说数据');
  71. die;
  72. }
  73. $novelDataList = $novelDataList->keyBy('id')->toArray();
  74. $accountConf = $accountConf->toArray();
  75. $novelData = [];
  76. foreach ($accountConf as $conf) {
  77. $value = [];
  78. $value['start_date'] = $conf['start_date'];
  79. $value['end_date'] = $conf['end_date'];
  80. $novelInfo = isset($novelDataList[$conf['novel_id']]) ? $novelDataList[$conf['novel_id']] : null;
  81. if(is_null($novelInfo)) {
  82. continue;
  83. }
  84. $i = $value['start_date'];
  85. while($i<=$value['end_date']) {
  86. $key = $conf['novel_id'].'###'.$i;
  87. if(isset($novelData[$key])) {
  88. $novelData[$key]['app_list'][] = $conf['app_id'];
  89. } else {
  90. $novelData[$key]['novel_id'] = $conf['novel_id'];
  91. $novelData[$key]['subscribed_date'] = $i;
  92. $novelData[$key]['app_list'][] = $conf['app_id'];
  93. }
  94. $i = date('Y-m-d', strtotime($i . ' +1 day'));
  95. }
  96. }
  97. if(empty($novelData)) {
  98. $this->infoMessage(' 小说数据整理数据结果为空');
  99. die;
  100. }
  101. foreach($novelData as $novel) {
  102. $value = json_encode($novel, JSON_UNESCAPED_UNICODE);
  103. RedisModel::lPush($this->redisKey, $value);
  104. }
  105. }
  106. public function consume()
  107. {
  108. $novelDataList = NovelConf::query()
  109. ->where('enable', 1)
  110. ->get();
  111. if(empty($novelDataList)) {
  112. $this->infoMessage(' 暂无启用的小说数据');
  113. return ;
  114. }
  115. $novelDataList = $novelDataList->keyBy('id')->toArray();
  116. $time = time();
  117. $val = null;
  118. while(1) {
  119. if(time() - $time >= 1200) {
  120. die;
  121. }
  122. $val = RedisModel::rPop($this->redisKey);
  123. if(empty($val)) {
  124. sleep(10);
  125. continue;
  126. }
  127. try{
  128. $this->handleData(json_decode($val, 1), $novelDataList);
  129. } catch (\Exception $exception) {
  130. // 异常情况下将数据重新塞入队列
  131. RedisModel::lPush($this->redisKey, $val);
  132. $this->infoMessage(' 异常###file:'.$exception->getFile().';line:'.$exception->getLine().';message:'.$exception->getMessage());
  133. }
  134. }
  135. }
  136. public function handleData($v, $novelDataList)
  137. {
  138. $novelInfo = isset($novelDataList[$v['novel_id']]) ? $novelDataList[$v['novel_id']] : null;
  139. if(is_null($novelInfo)) {
  140. return ;
  141. }
  142. $v['novel_name'] = $novelInfo['name'];
  143. $v['first_day_roi'] = 0;
  144. $v['new_user_cost'] = 0;
  145. $v['new_user_charge'] = 0;
  146. $v['day_paid'] = 0;
  147. $v['charge_total'] = 0;
  148. $v['cost_cover_rate'] = 0;
  149. $v['fans_increase'] = 0;
  150. $v['new_user_charge_uv'] = 0;
  151. $v['new_user_charge_pv'] = 0;
  152. $v['charge_data'] = '';
  153. $v['charge_user_cost'] = 0;
  154. $v['subscribe_date'] = $v['subscribed_date'];
  155. $v['start_date'] = $v['end_date'] = $v['subscribed_date'];
  156. $chargeData = [];
  157. $this->infoMessage(' 开始处理小说:'.$v['novel_name'].'在'.$v['start_date'].'日的数据;');
  158. # 查询公众号对应的adq账号列表
  159. if(!empty($v['app_list'])){
  160. $adqAccountList = ADQAccountBinding::query()
  161. ->whereIn('app_id', $v['app_list'])
  162. ->where('start_date', '<=', $v['subscribed_date'])
  163. ->where('end_date', '>=', $v['subscribed_date'])
  164. ->where('status', 1)
  165. ->where('enable', 1)
  166. ->get();
  167. $adqAccountIdList = $adqAccountList->isNotEmpty() ? array_column($adqAccountList->toArray(), 'account_id') : [];
  168. } else {
  169. $adqAccountIdList = [];
  170. $adqAccountList = null;
  171. }
  172. $res = NovelDataStatistics::paymentTrendByDay($v['app_list'], $adqAccountList, $v['subscribed_date']);
  173. if(empty($res)) {
  174. $this->infoMessage(' 小说:'.$v['novel_name'].'在'.$v['start_date'].'日的趋势数据为空');
  175. $res = [];
  176. } else {
  177. $res = json_decode(json_encode($res), 1);
  178. }
  179. if(empty($v['app_list'])) {
  180. $this->infoMessage(' 小说:'.$v['novel_name'].'在'.$v['start_date'].'日的app_list数据为空');
  181. # 当天消耗
  182. $v['day_paid'] = 0;
  183. # 新增粉丝数
  184. $v['fans_increase'] = 0;
  185. } else {
  186. # 数据计算处理以及赋予默认值
  187. $adsReport = AdsReport::query()
  188. ->selectRaw('sum(paid) as paid, sum(follow_uv) as follow_uv')
  189. ->whereIn('app_id', $v['app_list'])
  190. ->where('ref_date', $v['subscribed_date'])
  191. ->first();
  192. if(empty($adqAccountIdList)) {
  193. $adqAdsReport = null;
  194. } else {
  195. $adqAdsReport = AdsReport::query()
  196. ->selectRaw('sum(paid) as paid, sum(follow_uv) as follow_uv')
  197. ->whereIn('account_id', $adqAccountIdList)
  198. ->where('ref_date', $v['subscribed_date'])
  199. ->first();
  200. }
  201. # 当天消耗
  202. $v['day_paid'] = isset($adsReport->paid) ? $v['day_paid'] + $adsReport->paid / 100 : $v['day_paid'];
  203. $v['day_paid'] = isset($adqAdsReport->paid) ? $v['day_paid'] + $adqAdsReport->paid / 100 : $v['day_paid'];
  204. # 新增粉丝数
  205. $v['fans_increase'] = isset($adsReport->follow_uv) ? $v['fans_increase'] + $adsReport->follow_uv : $v['fans_increase'];
  206. $v['fans_increase'] = isset($adqAdsReport->follow_uv) ? $v['fans_increase'] + $adqAdsReport->follow_uv : $v['fans_increase'];
  207. }
  208. foreach($res as $item) {
  209. # 当日新用户累计充值
  210. $v['charge_total'] = isset($item['charge_total']) ? $v['charge_total'] + $item['charge_total'] : $v['charge_total'];
  211. # 充值人数
  212. $v['new_user_charge_uv'] = isset($item['new_user_charge_uv']) ? $v['new_user_charge_uv'] + $item['new_user_charge_uv'] : $v['new_user_charge_uv'];
  213. # 充值次数
  214. $v['new_user_charge_pv'] = isset($item['new_user_charge_pv']) ? $v['new_user_charge_pv'] + $item['new_user_charge_pv'] : $v['new_user_charge_pv'];
  215. # 开始日志至之后的100天数据
  216. for ($i = 0; $i < WxAccountService::DAYS; $i++) {
  217. if (isset($chargeData[$i])) {
  218. //已存在,累加
  219. $chargeData[$i]['day_charge'] = isset($item['chargeData'][$i]['day_charge']) ? $chargeData[$i]['day_charge'] + $item['chargeData'][$i]['day_charge'] : $chargeData[$i]['day_charge'];
  220. $chargeData[$i]['charge_total'] = isset($item['chargeData'][$i]['charge_total']) ? $chargeData[$i]['charge_total'] + $item['chargeData'][$i]['charge_total'] : $chargeData[$i]['charge_total'];
  221. $chargeData[$i]['day_paid'] = isset($item['chargeData'][$i]['day_paid']) ? $chargeData[$i]['day_paid'] + $item['chargeData'][$i]['day_paid'] : $chargeData[$i]['day_paid'];
  222. } else {
  223. //不存在,赋值
  224. $chargeData[$i]['day_charge'] = isset($item['chargeData'][$i]['day_charge']) ? $item['chargeData'][$i]['day_charge'] : 0;
  225. $chargeData[$i]['charge_total'] = isset($item['chargeData'][$i]['charge_total']) ? $item['chargeData'][$i]['charge_total'] : 0;
  226. $chargeData[$i]['day_paid'] = isset($item['chargeData'][$i]['day_paid']) ? $item['chargeData'][$i]['day_paid'] : 0;
  227. }
  228. }
  229. }
  230. /*上面两项不在循环中累加的原因在于,循环中重点统计有新增粉丝的数据,循环中累加会丢失部分没有新粉数据日期的消耗*/
  231. # 首日ROI
  232. $v['first_day_roi'] = isset($chargeData[0]['day_paid']) && isset($chargeData[0]['day_charge']) && $chargeData[0]['day_paid'] != 0 ? ($chargeData[0]['day_charge'] / $chargeData[0]['day_paid']) : 0;
  233. # 回本率
  234. $v['cost_cover_rate'] = $v['day_paid'] != 0 ? ($v['charge_total'] / $v['day_paid']) : 0;
  235. # 新增粉丝成本
  236. $v['new_user_cost'] = $v['fans_increase'] != 0 ? ($v['day_paid'] / $v['fans_increase']) : 0;
  237. # 充值用户成本
  238. $v['charge_user_cost'] = $v['new_user_charge_uv'] != 0 ? ($v['day_paid'] / $v['new_user_charge_uv']) : 0;
  239. for ($i = 0; $i < WxAccountService::DAYS; $i++) {
  240. $chargeData[$i]['day_charge'] = isset($chargeData[$i]['day_charge']) ? $chargeData[$i]['day_charge'] : 0;
  241. $chargeData[$i]['charge_total'] = isset($chargeData[$i]['charge_total']) ? $chargeData[$i]['charge_total'] : 0;
  242. $chargeData[$i]['day_paid'] = isset($chargeData[$i]['day_paid']) ? $chargeData[$i]['day_paid'] : 0;
  243. $chargeData[$i]['day_add'] = (isset($chargeData[$i]['day_paid']) && $chargeData[$i]['day_paid'] != 0) ? ($chargeData[$i]['day_charge'] / $chargeData[$i]['day_paid']) : 0;
  244. $chargeData[$i]['day_cover'] = (isset($chargeData[$i]['day_paid']) && $chargeData[$i]['day_paid'] != 0) ? ($chargeData[$i]['charge_total'] / $chargeData[$i]['day_paid']) : 0;
  245. $chargeData[$i]['day_times'] = isset($chargeData[$i]['day_paid']) && ($chargeData[$i]['day_paid'] * $v['first_day_roi']) != 0 ? ($chargeData[$i]['charge_total'] / ($chargeData[$i]['day_paid'] * $v['first_day_roi'])) : 0;
  246. }
  247. $v['charge_data'] = json_encode($chargeData);
  248. $this->insertData($v);
  249. }
  250. public function paymentTrendByDay($appId, $adqAccountList, $date)
  251. {
  252. # 获取对应的公众号AppId
  253. if(is_array($appId)){
  254. $appIdList = $appId;
  255. } else {
  256. $appIdList = [$appId];
  257. }
  258. # 查询账号信息
  259. $accountData = Account::query()->selectRaw("channel_id, platform_id, nickname, app_id")
  260. ->whereIn('app_id', $appIdList)->get();
  261. # 获取各日期对应的花费
  262. $paidList = AdsReport::getPaidData($appIdList, [$date]);
  263. # 查询adq账号消耗数据
  264. $adqAccountIdList = (!empty($adqAccountList) && $adqAccountList->isNotEmpty()) ?
  265. array_column($adqAccountList->toArray(), 'account_id') : [];
  266. if(!empty($adqAccountIdList)){
  267. $adqPaidList = AdsReport::getAdqPaidData($adqAccountIdList, [$date]);
  268. } else {
  269. $adqPaidList = null;
  270. }
  271. $result = [];
  272. foreach($appIdList as $app_id){
  273. $trend = [];
  274. $channelIdList = $accountData->where('app_id', $app_id)->pluck('channel_id')->toArray();
  275. $fansData = self::subscribedFansData($channelIdList, $date, $date);
  276. $fansData = json_decode(json_encode($fansData), 1);
  277. $trend['fans_total'] = !empty($fansData) ? $fansData['fans_total'] : 0;
  278. $trend['charge_total'] = !empty($fansData) ? $fansData['charge_total'] / 100 : 0;
  279. $trend['app_id'] = $app_id;
  280. # 当日消耗
  281. $paidInfo = $paidList->where('app_id', $app_id)
  282. ->where('ref_date', $date)
  283. ->first();
  284. $trend['day_paid'] = isset($paidInfo->paid) ? ($paidInfo->paid / 100) : 0;
  285. # adq消耗
  286. $adqAccountIdList = $adqAccountList->where('app_id', $app_id)->pluck('account_id')->toArray();
  287. $adqPaid = !empty($adqPaidList) && $adqPaidList->isNotEmpty() ?
  288. $adqPaidList->whereIn('account_id', $adqAccountIdList)->sum('paid') : 0;
  289. $trend['day_paid'] += $adqPaid/100;
  290. # 获取当日新用户充值情况
  291. $newUserCharge = Order::getNewUserChargeSecond($date, $channelIdList);
  292. $trend['new_user_charge'] = isset($newUserCharge->new_user_charge) ? $newUserCharge->new_user_charge : 0;
  293. $trend['new_user_charge_uv'] = isset($newUserCharge->new_user_charge_uv) ? $newUserCharge->new_user_charge_uv : 0;
  294. $trend['new_user_charge_pv'] = isset($newUserCharge->new_user_charge_pv) ? $newUserCharge->new_user_charge_pv : 0;
  295. $trend['first_day_roi'] = $trend['day_paid'] != 0 ? (($trend['new_user_charge'] / 100) / $trend['day_paid']) : 0;
  296. // 充增回倍
  297. $trend['chargeData'] = WxAccountService::chargeAfterSubscribed(
  298. $channelIdList, $date, WxAccountService::DAYS, $app_id, $trend['first_day_roi'], $trend['day_paid']
  299. );
  300. $result[] = $trend;
  301. }
  302. return $result;
  303. }
  304. /*
  305. * 获取时间段内新关注粉丝数据
  306. * */
  307. public function subscribedFansData($channelIds, $startDate, $endDate)
  308. {
  309. # 获取公众号数据
  310. $list = Order::query()
  311. ->selectRaw("DATE_FORMAT(user_created_at, '%Y-%m-%d') as subscribed_date,
  312. count(member_openid) as fans_total, channel_id, sum(price) as charge_total")
  313. ->where('enable', 1)
  314. ->where(function($query) use($channelIds) {
  315. if(!empty($channelIds))
  316. $query->whereIn('channel_id', $channelIds);
  317. })
  318. ->where('user_created_at', '>=', $startDate . ' 00:00:00')
  319. ->where('user_created_at', '<=', $endDate . ' 23:59:59')
  320. ->where('status', 1)
  321. ->first();
  322. return $list;
  323. }
  324. /*
  325. * 付费趋势(废弃)
  326. * */
  327. public function paymentTrend($appId, $startDate, $endDate, $page, $pageSize)
  328. {
  329. # 获取对应的公众号AppId
  330. if(is_array($appId)){
  331. $appIdList = $appId;
  332. } else {
  333. $appIdList = [$appId];
  334. }
  335. $channelIdList = Account::getChannelIdByAppId($appIdList);
  336. $channelIdList = $channelIdList->toArray();
  337. // $channelIds = array_column($channelIdList, 'channel_id');
  338. # 获取不同时间段内新增的粉丝充值信息
  339. // list($fansData, $total) = Order::subscribedFansData($channelIds, '2021-09-22', '2021-09-22', $page, $pageSize);
  340. list($fansData, $total) = Order::subscribedFansData($channelIdList, $startDate, $endDate, $page, $pageSize);
  341. # 获取列表内的日期
  342. $dateList = array_unique(array_column($fansData->toArray(), 'subscribed_date'));
  343. if(empty($dateList)) return [[], 0];
  344. # 渠道ID集合
  345. $channelIdArr = array_unique(array_column($fansData->toArray(), 'channel_id'));;
  346. # 获取公众号信息
  347. $accountData = Account::getAccountListByChannelIds($channelIdArr);
  348. # 获取各日期对应的花费
  349. $paidList = AdsReport::getPaidData($appIdList, $dateList);
  350. foreach($fansData as $trend) {
  351. $trend->charge_total = $trend->charge_total / 100;
  352. $trend->app_id = isset($accountData[$trend->channel_id]['app_id']) ? $accountData[$trend->channel_id]['app_id'] : null;
  353. # 当日消耗
  354. $paidInfo = $paidList->where('app_id', $trend->app_id)
  355. ->where('ref_date', $trend->subscribed_date)
  356. ->first();
  357. $trend->day_paid = isset($paidInfo->paid) ? ($paidInfo->paid / 100) : 0;
  358. # 获取当日新用户充值情况
  359. $newUserCharge = Order::getNewUserCharge($trend->subscribed_date, $trend->channel_id);
  360. $trend->new_user_charge = isset($newUserCharge->new_user_charge) ? $newUserCharge->new_user_charge : 0;
  361. $trend->new_user_charge_uv = isset($newUserCharge->new_user_charge_uv) ? $newUserCharge->new_user_charge_uv : 0;
  362. $trend->new_user_charge_pv = isset($newUserCharge->new_user_charge_pv) ? $newUserCharge->new_user_charge_pv : 0;
  363. $trend->first_day_roi = $trend->day_paid != 0 ? (($trend->new_user_charge / 100) / $trend->day_paid) : 0;
  364. // 充增回倍
  365. $trend->chargeData = WxAccountService::chargeAfterSubscribed(
  366. [$trend->channel_id], $trend->subscribed_date, WxAccountService::DAYS, $trend->app_id, $trend->first_day_roi, $trend->day_paid
  367. );
  368. }
  369. return [$fansData, $total];
  370. }
  371. public function insertData($novel)
  372. {
  373. $insertData['start_date'] = $novel['start_date'];
  374. $insertData['end_date'] = $novel['end_date'];
  375. $insertData['novel_name'] = $novel['novel_name'];
  376. $insertData['novel_id'] = $novel['novel_id'];
  377. $insertData['first_day_roi'] = $novel['first_day_roi'];
  378. $insertData['day_paid'] = $novel['day_paid'];
  379. $insertData['charge_total'] = $novel['charge_total'];
  380. $insertData['cost_cover_rate'] = $novel['cost_cover_rate'];
  381. $insertData['fans_increase'] = $novel['fans_increase'];
  382. $insertData['new_user_cost'] = $novel['new_user_cost'];
  383. $insertData['new_user_charge_uv'] = $novel['new_user_charge_uv'];
  384. $insertData['new_user_charge_pv'] = $novel['new_user_charge_pv'];
  385. $insertData['charge_user_cost'] = $novel['charge_user_cost'];
  386. $insertData['charge_data'] = $novel['charge_data'];
  387. $row = NovelData::query()
  388. ->where('novel_id', $insertData['novel_id'])
  389. ->where('start_date', $insertData['start_date'])
  390. ->first();
  391. if($row) {
  392. # 更新
  393. $res = NovelData::query()
  394. ->where('novel_id', $insertData['novel_id'])
  395. ->where('start_date', $insertData['start_date'])
  396. ->update($insertData);
  397. if($res) {
  398. # 更新成功
  399. $this->infoMessage(' 处理小说:'.$insertData['novel_name'].'在'.$insertData['start_date'].'日的数据完成;处理结果:更新成功');
  400. } else {
  401. # 未有修改
  402. $this->infoMessage(' 处理小说:'.$insertData['novel_name'].'在'.$insertData['start_date'].'日的数据完成;处理结果:更新失败');
  403. }
  404. } else {
  405. $insertData['created_date'] = date('Y-m-d');
  406. # 插入
  407. $res = NovelData::query()
  408. ->insert($insertData);
  409. if($res) {
  410. # 新增成功
  411. $this->infoMessage(' 处理小说:'.$insertData['novel_name'].'在'.$insertData['start_date'].'日的数据完成;处理结果:新增成功');
  412. } else {
  413. # 新增失败
  414. $this->infoMessage(' 处理小说:'.$insertData['novel_name'].'在'.$insertData['start_date'].'日的数据完成;处理结果:新增失败');
  415. }
  416. }
  417. }
  418. public function infoMessage($message)
  419. {
  420. echo date('Y-m-d H:i:s') . $message .' 内存占用:'.round(memory_get_usage()/1024/1024, 2).'MB'. "\r\n";
  421. }
  422. }