ComplexSummaryService.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. /**
  5. * 聚合数据统计服务类
  6. * 用于统一处理聚合数据统计的公共查询方法
  7. */
  8. class ComplexSummaryService
  9. {
  10. /**
  11. * 将查询结果统一转换为数组,兼容 Collection/array
  12. * @param mixed $result
  13. * @return array
  14. */
  15. protected function normalizeSelectResult($result)
  16. {
  17. if (empty($result)) {
  18. return [];
  19. }
  20. if (is_array($result)) {
  21. return $result;
  22. }
  23. return $result->toArray();
  24. }
  25. /**
  26. * 获取已上线游戏ID列表
  27. * @return array
  28. */
  29. public function getOnlineGameIds()
  30. {
  31. return model('common/Game')
  32. ->cache('complex:online_game_ids', 500)
  33. ->where('cooperation_status', 'in', [1, 2])
  34. ->column('id');
  35. }
  36. /**
  37. * 构建基础查询条件
  38. * @param string $day 日期 (格式: Y-m-d)
  39. * @return array 返回时间范围数组
  40. */
  41. public function buildTimeRange($day)
  42. {
  43. $startTime = strtotime($day);
  44. $endTime = strtotime($day . ' 23:59:59');
  45. return [$startTime, $endTime];
  46. }
  47. /**
  48. * 构建分组字段和GROUP BY子句
  49. * @param int $hasGame 是否按游戏分组 (0=否, 1=是)
  50. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  51. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  52. * @param string $gameField 游戏ID字段名(默认:gameid,用于members和pay表)
  53. * @param string $channelField 渠道ID字段名(默认:channel_id,用于members和pay表)
  54. * @return array ['field' => 字段字符串, 'group' => GROUP BY数组]
  55. */
  56. protected function buildGroupField($hasGame = 1, $hasChannel = 1, $hasServer = 0, $gameField = 'gameid', $channelField = 'channel_id')
  57. {
  58. $field = '';
  59. $group = [];
  60. if ($hasGame) {
  61. $field .= $gameField . ' as game_id,';
  62. $group[] = $gameField;
  63. } else {
  64. $field .= '0 as game_id,';
  65. }
  66. if ($hasChannel) {
  67. $field .= $channelField . ' as channel_id,';
  68. $group[] = $channelField;
  69. } else {
  70. $field .= '0 as channel_id,';
  71. }
  72. if ($hasServer) {
  73. $field .= 'server_id,';
  74. $group[] = 'server_id';
  75. } else {
  76. $field .= '0 as server_id,';
  77. }
  78. return [
  79. 'field' => $field,
  80. 'group' => $group
  81. ];
  82. }
  83. /**
  84. * 新增玩家统计
  85. * @param string $day 日期
  86. * @param array $gameIds 游戏ID列表(可选,为空则统计所有已上线游戏)
  87. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  88. * @return array
  89. */
  90. public function getRegNum($day, $gameIds = [], $hasChannel = 1)
  91. {
  92. list($startTime, $endTime) = $this->buildTimeRange($day);
  93. $groupInfo = $this->buildGroupField(1, $hasChannel, 0);
  94. $sql = Db::table('nw_complex_members')->whereBetween('reg_time', [$startTime, $endTime]);
  95. // 只统计已上线游戏
  96. if (empty($gameIds)) {
  97. $gameIds = $this->getOnlineGameIds();
  98. }
  99. if (!empty($gameIds)) {
  100. $sql->where('gameid', 'in', $gameIds);
  101. }
  102. $field = $groupInfo['field'] . 'count(DISTINCT id) as reg_num';
  103. $sql->field($field);
  104. if (!empty($groupInfo['group'])) {
  105. $sql->group(implode(',', $groupInfo['group']));
  106. }
  107. // dump($sql->fetchSql(true)->select());
  108. return $this->normalizeSelectResult($sql->select());
  109. }
  110. /**
  111. * 新增角色统计
  112. * @param string $day 日期
  113. * @param array $gameIds 游戏ID列表(可选)
  114. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  115. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  116. * @return array
  117. */
  118. public function getRegRoleNum($day, $gameIds = [], $hasChannel = 1, $hasServer = 0)
  119. {
  120. list($startTime, $endTime) = $this->buildTimeRange($day);
  121. // 角色表使用 game_id 和 complex_id
  122. $groupInfo = $this->buildGroupField(1, $hasChannel, $hasServer, 'game_id', 'complex_id');
  123. $sql = Db::table('nw_complex_role')->whereBetween('create_time', [$startTime, $endTime]);
  124. if (empty($gameIds)) {
  125. $gameIds = $this->getOnlineGameIds();
  126. }
  127. if (!empty($gameIds)) {
  128. $sql->where('game_id', 'in', $gameIds);
  129. }
  130. // 如果按区服分组,确保 server_id 不为 NULL,避免多个区服的数据被合并
  131. // if ($hasServer) {
  132. // $sql->whereNotNull('server_id');
  133. // }
  134. $field = $groupInfo['field'] . ' count(DISTINCT id) as reg_role_num';
  135. $sql->field($field);
  136. if (!empty($groupInfo['group'])) {
  137. $sql->group(implode(',', $groupInfo['group']));
  138. }
  139. // dump($sql->fetchSql(true)->select());
  140. $result = $this->normalizeSelectResult($sql->select());
  141. // 如果按区服分组,批量查询区服名称
  142. if ($hasServer && !empty($result)) {
  143. // 收集所有的 server_id
  144. $serverIds = array_filter(array_unique(array_column($result, 'server_id')));
  145. // 批量查询区服名称
  146. $serverNames = [];
  147. if (!empty($serverIds)) {
  148. $serverList = Db::table('nw_complex_server')
  149. ->where('id', 'in', $serverIds)
  150. ->column('server_name', 'id');
  151. $serverNames = $serverList ?: [];
  152. }
  153. // 合并区服名称到结果中
  154. foreach ($result as &$item) {
  155. $item['server_name'] = $serverNames[$item['server_id']] ?? '';
  156. }
  157. unset($item);
  158. }
  159. return $result;
  160. }
  161. /**
  162. * 充值金额和付费人数统计
  163. * @param string $day 日期
  164. * @param array $gameIds 游戏ID列表(可选)
  165. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  166. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  167. * @return array
  168. */
  169. public function getPayMoney($day, $gameIds = [], $hasChannel = 1, $hasServer = 0)
  170. {
  171. list($startTime, $endTime) = $this->buildTimeRange($day);
  172. // 支付表使用 gameid 和 channel_id
  173. $groupInfo = $this->buildGroupField(1, $hasChannel, $hasServer, 'gameid', 'channel_id');
  174. $sql = Db::table('nw_complex_pay')
  175. ->whereBetween('create_time', [$startTime, $endTime])
  176. ->where('status', 1); // 只统计成功订单
  177. if (empty($gameIds)) {
  178. $gameIds = $this->getOnlineGameIds();
  179. }
  180. if (!empty($gameIds)) {
  181. $sql->where('gameid', 'in', $gameIds);
  182. }
  183. // , count(DISTINCT userid) as pay_user_num
  184. $field = $groupInfo['field'] . 'sum(amount) as pay_money, sum(pay_amount) as pay_amount_total, count(DISTINCT userid) as pay_user_num';
  185. $sql->field($field);
  186. if (!empty($groupInfo['group'])) {
  187. $sql->group(implode(',', $groupInfo['group']));
  188. }
  189. // dump($sql->fetchSql(true)->select());
  190. return $this->normalizeSelectResult($sql->select());
  191. }
  192. /**
  193. * 活跃玩家统计(基于玩家活跃日志表 nw_complex_player_active_log)
  194. * @param string $day 日期
  195. * @param array $gameIds 游戏ID列表(可选)
  196. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  197. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  198. * @return array
  199. */
  200. public function getActiveUserByActiveLog($day, $gameIds = [], $hasChannel = 1, $hasServer = 0)
  201. {
  202. $startTime = $day . ' 00:00:00';
  203. $endTime = $day . ' 23:59:59';
  204. $groupInfo = $this->buildGroupField(1, $hasChannel, $hasServer, 'game_id', 'complex_id');
  205. $sql = Db::table('nw_complex_player_active_log')
  206. ->whereBetween('active_hour', [$startTime, $endTime]);
  207. if (empty($gameIds)) {
  208. $gameIds = $this->getOnlineGameIds();
  209. }
  210. if (!empty($gameIds)) {
  211. $sql->where('game_id', 'in', $gameIds);
  212. }
  213. $field = $groupInfo['field'] . 'count(DISTINCT member_id) as active_user_num';
  214. $sql->field($field);
  215. if (!empty($groupInfo['group'])) {
  216. $sql->group(implode(',', $groupInfo['group']));
  217. }
  218. $result = $this->normalizeSelectResult($sql->select());
  219. if ($hasServer && !empty($result)) {
  220. $serverIds = array_filter(array_unique(array_column($result, 'server_id')));
  221. $serverNames = [];
  222. if (!empty($serverIds)) {
  223. $serverNames = Db::table('nw_complex_server')
  224. ->where('id', 'in', $serverIds)
  225. ->column('server_name', 'id') ?: [];
  226. }
  227. foreach ($result as &$item) {
  228. $item['server_name'] = $serverNames[$item['server_id']] ?? '';
  229. }
  230. unset($item);
  231. }
  232. return $result;
  233. }
  234. /**
  235. * 活跃玩家统计(基于登录日志表 nw_complex_loginlog)
  236. * @param string $day 日期
  237. * @param array $gameIds 游戏ID列表(可选)
  238. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  239. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  240. * @return array
  241. */
  242. public function getActiveUserByLoginLog($day, $gameIds = [], $hasChannel = 1, $hasServer = 0)
  243. {
  244. list($startTime, $endTime) = $this->buildTimeRange($day);
  245. // 登录日志表 nw_complex_loginlog 使用 gameid 和 channel_id;表中没有区服字段,这里不做区服分组
  246. $groupInfo = $this->buildGroupField(1, $hasChannel, 0, 'gameid', 'channel_id');
  247. $sql = Db::table('nw_complex_loginlog')
  248. ->whereBetween('login_time', [$startTime, $endTime]);
  249. if (empty($gameIds)) {
  250. $gameIds = $this->getOnlineGameIds();
  251. }
  252. if (!empty($gameIds)) {
  253. $sql->where('gameid', 'in', $gameIds);
  254. }
  255. // 基于登录日志统计活跃用户数,按去重 userid 统计
  256. $field = $groupInfo['field'] . 'count(DISTINCT userid) as active_user_num';
  257. $sql->field($field);
  258. if (!empty($groupInfo['group'])) {
  259. $sql->group(implode(',', $groupInfo['group']));
  260. }
  261. // dump($sql->fetchSql(true)->select());
  262. return $this->normalizeSelectResult($sql->select());
  263. }
  264. /**
  265. * 活跃玩家统计(基于登录日志表)(用于游戏数据、渠道数据 统计)
  266. *
  267. * @param string $day 日期
  268. * @param array $gameIds 游戏ID列表(可选)
  269. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  270. * @param int $hasServer 是否按区服分组 (0=否, 1=是) 注:登录日志表无区服字段,此参数暂不生效
  271. * @param int $platform 是否按平台 (0=否, 1=是)
  272. * @return array
  273. */
  274. public function getActiveUserByMember($day, $gameIds = [], $hasChannel = 1)
  275. {
  276. list($startTime, $endTime) = $this->buildTimeRange($day);
  277. // 登录日志表使用 gameid 和 channel_id 字段,不支持区服分组(表中无 server_id 字段)
  278. $groupInfo = $this->buildGroupField(1, $hasChannel, 0, 'gameid', 'channel_id');
  279. $loginQuery = Db::table('nw_complex_loginlog')
  280. ->whereBetween('login_time', [$startTime, $endTime]);
  281. if (empty($gameIds)) {
  282. $gameIds = $this->getOnlineGameIds();
  283. }
  284. if (!empty($gameIds)) {
  285. $loginQuery->where('gameid', 'in', $gameIds);
  286. }
  287. // 统计活跃用户数(基于去重的 userid)
  288. $field = $groupInfo['field'] . 'count(DISTINCT userid) as active_user_num';
  289. $loginQuery->field($field);
  290. if (!empty($groupInfo['group'])) {
  291. $loginQuery->group(implode(',', $groupInfo['group']));
  292. }
  293. // dump($loginQuery->fetchSql(true)->select());
  294. return $this->normalizeSelectResult($loginQuery->select());
  295. }
  296. /**
  297. * 付费人数统计
  298. * @param string $day 日期
  299. * @param array $gameIds 游戏ID列表(可选)
  300. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  301. * @return array
  302. */
  303. public function getPayUserNum($day, $gameIds = [], $hasChannel = 1)
  304. {
  305. list($startTime, $endTime) = $this->buildTimeRange($day);
  306. // 支付表使用 gameid 和 channel_id
  307. $groupInfo = $this->buildGroupField(1, $hasChannel, 0, 'gameid', 'channel_id');
  308. $payQuery = Db::table('nw_complex_pay')
  309. ->whereBetween('create_time', [$startTime, $endTime])
  310. ->where('status', 1); // 只统计成功订单
  311. if (!empty($gameIds)) {
  312. $payQuery->where('gameid', 'in', $gameIds);
  313. }
  314. $field = $groupInfo['field'] . 'count(DISTINCT userid) as pay_user_num';
  315. $payQuery->field($field);
  316. if (!empty($groupInfo['group'])) {
  317. $payQuery->group(implode(',', $groupInfo['group']));
  318. }
  319. // dump($payQuery->fetchSql(true)->select());
  320. return $this->normalizeSelectResult($payQuery->select());
  321. }
  322. /**
  323. * 新用户付费人数统计(当天注册且当天有充值)
  324. * @param string $day 日期
  325. * @param array $gameIds 游戏ID列表(可选)
  326. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  327. * @return array
  328. */
  329. public function getNewUserPayNum($day, $gameIds = [], $hasChannel = 1)
  330. {
  331. list($startTime, $endTime) = $this->buildTimeRange($day);
  332. $groupInfo = $this->buildGroupField(1, $hasChannel, 0, 'ncp.gameid', 'ncp.channel_id');
  333. $payQuery = Db::table('nw_complex_pay')->alias('ncp')
  334. ->join('nw_complex_members ncm', 'ncp.userid = ncm.id', 'left')
  335. ->whereBetween('ncp.create_time', [$startTime, $endTime])
  336. ->whereBetween('ncm.reg_time', [$startTime, $endTime])
  337. ->where('ncp.status', 1); // 只统计成功订单
  338. if (!empty($gameIds)) {
  339. $payQuery->where('ncp.gameid', 'in', $gameIds);
  340. }
  341. $field = $groupInfo['field'] . 'count(DISTINCT ncp.userid) as reg_pay_num';
  342. $payQuery->field($field);
  343. if (!empty($groupInfo['group'])) {
  344. $payQuery->group(implode(',', $groupInfo['group']));
  345. }
  346. return $this->normalizeSelectResult($payQuery->select());
  347. }
  348. /**
  349. * 新用户充值金额统计
  350. * @param string $day 日期
  351. * @param array $gameIds 游戏ID列表(可选)
  352. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  353. * @return array
  354. */
  355. public function getNewUserPayMoney($day, $gameIds = [], $hasChannel = 1)
  356. {
  357. list($startTime, $endTime) = $this->buildTimeRange($day);
  358. // 1. 先从用户表中获取当天注册的新用户ID
  359. $memberQuery = Db::table('nw_complex_members')
  360. ->whereBetween('reg_time', [$startTime, $endTime]);
  361. if (!empty($gameIds)) {
  362. $memberQuery->where('gameid', 'in', $gameIds);
  363. }
  364. // 这里使用玩家在 members 表中的主键 id 作为 userid,与 nw_complex_pay.userid 对应
  365. $newUserIds = $memberQuery->column('DISTINCT id');
  366. if (empty($newUserIds)) {
  367. return [];
  368. }
  369. // 2. 再到支付表中,统计这些新用户在当天的充值金额
  370. // 支付表使用 gameid 和 channel_id
  371. $groupInfo = $this->buildGroupField(1, $hasChannel, 0, 'gameid', 'channel_id');
  372. $payQuery = Db::table('nw_complex_pay')
  373. ->whereBetween('create_time', [$startTime, $endTime])
  374. ->where('status', 1) // 只统计成功订单
  375. ->where('userid', 'in', $newUserIds);
  376. if (!empty($gameIds)) {
  377. $payQuery->where('gameid', 'in', $gameIds);
  378. }
  379. $field = $groupInfo['field'] . 'sum(amount) as reg_pay_money, sum(pay_amount) as reg_pay_money_total';
  380. $payQuery->field($field);
  381. if (!empty($groupInfo['group'])) {
  382. $payQuery->group(implode(',', $groupInfo['group']));
  383. }
  384. return $this->normalizeSelectResult($payQuery->select());
  385. }
  386. /**
  387. * 玩家总数统计(累计到指定日期)
  388. * @param string $day 日期
  389. * @param array $gameIds 游戏ID列表(可选)
  390. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  391. * @param int $hasServer 是否按区服分组 (0=否, 1=是)
  392. * @return array
  393. */
  394. public function getMemberTotal($day, $gameIds = [], $hasChannel = 1, $hasServer = 0)
  395. {
  396. $endTime = strtotime($day . ' 23:59:59');
  397. $groupInfo = $this->buildGroupField(1, $hasChannel, $hasServer);
  398. $sql = model('common/ComplexMembers')
  399. ->where('reg_time', '<', $endTime);
  400. if (empty($gameIds)) {
  401. $gameIds = $this->getOnlineGameIds();
  402. }
  403. if (!empty($gameIds)) {
  404. $sql->where('gameid', 'in', $gameIds);
  405. }
  406. $field = $groupInfo['field'] . 'count(DISTINCT id) as reg_total';
  407. $sql->field($field);
  408. if (!empty($groupInfo['group'])) {
  409. $sql->group(implode(',', $groupInfo['group']));
  410. }
  411. return $this->normalizeSelectResult($sql->select());
  412. }
  413. /**
  414. * 基于注册时间的留存计算
  415. * @param string $targetDay 目标日(当前日期)
  416. * @param int $dayOffset 注册日相对目标登录日的偏移天数
  417. * @param array $gameIds 游戏ID列表(可选)
  418. * @param int $hasChannel 是否按渠道分组 (0=否, 1=是)
  419. * @return array
  420. */
  421. public function getRetentionByRegTime($targetDay, $dayOffset, $gameIds = [], $hasChannel = 1)
  422. {
  423. // 计算注册日:次留偏移1天,3留偏移2天,以此类推
  424. $baseDay = date('Y-m-d', strtotime($targetDay . " -{$dayOffset} days"));
  425. // 基准日时间范围
  426. $baseStartTime = strtotime($baseDay);
  427. $baseEndTime = strtotime($baseDay . ' 23:59:59');
  428. // 目标日活跃时间范围(玩家活跃日志使用 DATETIME)
  429. $targetStartTime = $targetDay . ' 00:00:00';
  430. $targetEndTime = $targetDay . ' 23:59:59';
  431. // 1. 获取基准日注册的所有用户ID(按游戏和渠道分组)
  432. $baseDayUsers = model('common/ComplexMembers')
  433. ->whereBetween('reg_time', [$baseStartTime, $baseEndTime]);
  434. if (empty($gameIds)) {
  435. $gameIds = $this->getOnlineGameIds();
  436. }
  437. if (!empty($gameIds)) {
  438. $baseDayUsers->where('gameid', 'in', $gameIds);
  439. }
  440. $baseDayUsersList = $baseDayUsers
  441. ->field('id, gameid as game_id, channel_id as complex_id')
  442. ->select();
  443. if (empty($baseDayUsersList)) {
  444. return [];
  445. }
  446. // 按游戏和渠道分组,统计基准日总用户数
  447. $baseStats = [];
  448. $userIdsByGroup = [];
  449. foreach ($baseDayUsersList as $user) {
  450. $key = $user['game_id'] . '_' . $user['complex_id'];
  451. if (!isset($baseStats[$key])) {
  452. $baseStats[$key] = [
  453. 'game_id' => $user['game_id'],
  454. 'channel_id' => $user['complex_id'],
  455. 'base_reg_num' => 0,
  456. 'user_ids' => []
  457. ];
  458. }
  459. $baseStats[$key]['base_reg_num']++;
  460. $baseStats[$key]['user_ids'][] = $user['id'];
  461. $userIdsByGroup[$key] = $baseStats[$key]['user_ids'];
  462. }
  463. // 2. 查询这些用户在目标日是否活跃(基于玩家活跃日志)
  464. $result = [];
  465. foreach ($userIdsByGroup as $key => $userIds) {
  466. $activeCount = Db::table('nw_complex_player_active_log')
  467. ->where('member_id', 'in', $userIds)
  468. ->where('game_id', $baseStats[$key]['game_id'])
  469. ->where('complex_id', $baseStats[$key]['channel_id'])
  470. ->whereBetween('active_hour', [$targetStartTime, $targetEndTime])
  471. ->count('DISTINCT member_id');
  472. $baseRegNum = $baseStats[$key]['base_reg_num'];
  473. $retentionRate = $baseRegNum > 0 ? round(100 * $activeCount / $baseRegNum, 2) : 0;
  474. $result[] = [
  475. 'game_id' => $baseStats[$key]['game_id'],
  476. 'channel_id' => $baseStats[$key]['channel_id'],
  477. 'base_reg_num' => $baseRegNum,
  478. 'retention_num' => $activeCount,
  479. 'retention_rate' => $retentionRate
  480. ];
  481. }
  482. return $result;
  483. }
  484. /**
  485. * 批量计算多种留存率。
  486. * targetDay 是登录统计日,每项结果通过 stat_day 标记应回写的注册日。
  487. *
  488. * @param string $targetDay 登录统计日
  489. * @param array $gameIds 游戏ID列表
  490. * @param int $hasChannel 是否按渠道分组
  491. * @return array
  492. */
  493. public function getBatchRetention($targetDay, $gameIds = [], $hasChannel = 1)
  494. {
  495. $retentionConfig = [
  496. 'one_stay' => 1,
  497. 'three_stay' => 2,
  498. 'four_stay' => 3,
  499. 'five_stay' => 4,
  500. 'six_stay' => 5,
  501. 'seven_stay' => 6,
  502. 'fifteen_stay' => 14,
  503. 'thirty_stay' => 29,
  504. ];
  505. $result = [];
  506. foreach ($retentionConfig as $fieldName => $dayOffset) {
  507. $statDay = date('Y-m-d', strtotime($targetDay . " -{$dayOffset} days"));
  508. $data = $this->getRetentionByRegTime($targetDay, $dayOffset, $gameIds, $hasChannel);
  509. // 不同留存字段属于不同注册日,不能再按执行日合并到同一行
  510. foreach ($data as $item) {
  511. $result[] = [
  512. 'stat_day' => $statDay,
  513. 'game_id' => $item['game_id'],
  514. 'channel_id' => $item['channel_id'],
  515. 'base_reg_num' => $item['base_reg_num'],
  516. 'retention_num' => $item['retention_num'],
  517. 'retention_field' => $fieldName,
  518. 'retention_rate' => $item['retention_rate'],
  519. ];
  520. }
  521. }
  522. return $result;
  523. }
  524. /**
  525. * 合并统计数据(将多个统计结果按game_id和complex_id合并)
  526. * @param array $dataList 统计数据列表
  527. * @return array 合并后的数据
  528. */
  529. public function mergeStatsData($dataList)
  530. {
  531. $result = [];
  532. foreach ($dataList as $data) {
  533. foreach ($data as $item) {
  534. // 合并键需要包含 game_id、channel_id 和 server_id,以区分不同的区服
  535. $serverId = $item['server_id'] ?? 0;
  536. $key = ($item['game_id'] ?? 0) . '_' . ($item['channel_id'] ?? 0) . '_' . $serverId;
  537. if (!isset($result[$key])) {
  538. $result[$key] = [
  539. 'game_id' => $item['game_id'] ?? 0,
  540. 'channel_id' => $item['channel_id'] ?? 0,
  541. 'server_id' => $serverId,
  542. ];
  543. }
  544. // 合并其他字段
  545. foreach ($item as $k => $v) {
  546. if (!in_array($k, ['game_id', 'channel_id', 'server_id'])) {
  547. // reg_total 是累计值,应该取最大值而不是累加
  548. if ($k === 'reg_total') {
  549. if (isset($result[$key][$k])) {
  550. $result[$key][$k] = max($result[$key][$k], $v);
  551. } else {
  552. $result[$key][$k] = $v;
  553. }
  554. } elseif ($k === 'reg_num') {
  555. // reg_num 表示目标日新增玩家数,不与留存计算中的基准注册数累加
  556. if (!isset($result[$key][$k])) {
  557. $result[$key][$k] = $v;
  558. }
  559. } elseif (isset($result[$key][$k])) {
  560. // 如果是数值字段,累加;如果是字符串字段(如 server_name),取最后一个非空值
  561. if (is_numeric($v) && is_numeric($result[$key][$k])) {
  562. $result[$key][$k] += $v;
  563. } elseif (!empty($v)) {
  564. $result[$key][$k] = $v;
  565. }
  566. } else {
  567. $result[$key][$k] = $v;
  568. }
  569. }
  570. }
  571. }
  572. }
  573. return array_values($result);
  574. }
  575. }