MemberActivationRecordLogic.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <?php
  2. namespace plugin\saiadmin\app\logic\fund;
  3. use DateTime;
  4. use plugin\saiadmin\app\model\system\SystemUser;
  5. use plugin\saiadmin\basic\think\BaseLogic;
  6. use plugin\saiadmin\exception\ApiException;
  7. use plugin\saiadmin\service\OpenSpoutWriter;
  8. use support\Response;
  9. use support\think\Db;
  10. /**
  11. * 小程序用户会员开通与续费记录查询、汇总和导出。
  12. */
  13. class MemberActivationRecordLogic extends BaseLogic
  14. {
  15. private const SOURCE_NAMES = [1 => '微信', 2 => '支付宝', 3 => '后台开通'];
  16. private const CARD_TYPE_NAMES = [0 => '月卡', 1 => '季卡', 2 => '半年卡', 3 => '年卡'];
  17. private const DEVICE_NAMES = [
  18. 1 => '充电底座',
  19. 100 => 'K霸宝',
  20. 101 => '充电宝',
  21. 102 => 'K霸宝 Blue',
  22. 103 => '密码线充',
  23. 104 => '蓝牙线充',
  24. 105 => '密码蓝牙线充',
  25. 107 => '车载充电器',
  26. ];
  27. private const STATUS_NAMES = [0 => '开通失败', 1 => '开通成功'];
  28. private const EXPORT_LIMIT = 50000;
  29. public function indexList(array $filters): array
  30. {
  31. $page = max(1, (int) request()->input('page', 1));
  32. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  33. $result = $this->buildQuery($filters)
  34. ->order('r.activation_time', 'desc')
  35. ->order('r.id', 'desc')
  36. ->paginate(['page' => $page, 'list_rows' => $limit])
  37. ->toArray();
  38. $result['data'] = array_map(fn (array $row): array => $this->formatRecord($row), $result['data'] ?? []);
  39. $result['summary'] = $this->summary($filters);
  40. return $result;
  41. }
  42. public function export(array $filters): Response
  43. {
  44. $query = $this->buildQuery($filters);
  45. $count = (int) (clone $query)->count();
  46. if ($count > self::EXPORT_LIMIT) {
  47. throw new ApiException('导出记录超过 50000 条,请缩小筛选范围');
  48. }
  49. $rows = $query->order('r.activation_time', 'desc')->order('r.id', 'desc')->select()->toArray();
  50. $data = [];
  51. foreach ($rows as $row) {
  52. $record = $this->formatRecord($row);
  53. $data[] = [
  54. 'merchant_order_sn' => $this->excelSafe($record['merchant_order_sn']),
  55. 'transaction_sn' => $this->excelSafe($record['transaction_sn']),
  56. 'user_id' => $record['user_id'],
  57. 'nickname' => $this->excelSafe($record['nickname']),
  58. 'phone' => $this->excelSafe($record['phone']),
  59. 'source_name' => $record['source_name'],
  60. 'device_type_name' => $record['device_type_name'],
  61. 'merchant_name' => $this->excelSafe($record['merchant_name']),
  62. 'pay_amount' => $record['pay_amount'],
  63. 'card_type_name' => $record['card_type_name'],
  64. 'activation_time' => $record['activation_time'],
  65. 'expire_time' => $record['expire_time'],
  66. 'status_name' => $record['status_name'],
  67. 'remark' => $this->excelSafe($record['remark']),
  68. ];
  69. }
  70. $fileName = '会员开通记录_' . date('Ymd_His') . '.xlsx';
  71. $writer = new OpenSpoutWriter($fileName);
  72. $writer->setWidth([26, 28, 12, 22, 18, 12, 18, 24, 14, 12, 22, 22, 12, 36]);
  73. $writer->setHeader([
  74. '商户单号', '交易单号', '用户ID', '用户昵称', '手机号', '用户来源', '设备类型',
  75. '商户', '支付金额', '卡类型', '开通时间', '到期时间', '状态', '备注',
  76. ]);
  77. $writer->setData($data);
  78. return response()->download($writer->returnFile(), urlencode($fileName));
  79. }
  80. private function buildQuery(array $filters)
  81. {
  82. $query = Db::table('sa_member_activation_record')->alias('r')
  83. ->leftJoin('sa_rental_user u', 'u.id = r.user_id AND u.delete_time IS NULL')
  84. ->leftJoin('sa_merchant m', 'm.id = r.merchant_id AND m.delete_time IS NULL')
  85. ->whereNull('r.delete_time')
  86. ->field(implode(',', [
  87. 'r.id', 'r.merchant_order_sn', 'r.transaction_sn', 'r.user_id', 'r.manage_agent_id',
  88. 'r.source', 'r.device_type', 'r.merchant_id', 'r.merchant_name AS merchant_name_snapshot',
  89. 'r.pay_amount', 'r.card_type', 'r.activation_time', 'r.expire_time', 'r.status', 'r.remark',
  90. 'u.nickname', 'u.phone', 'u.avatar', 'm.name AS current_merchant_name',
  91. ]));
  92. $managerIds = $this->accessibleManagerIds();
  93. if (is_array($managerIds)) {
  94. if ($managerIds === []) {
  95. $query->where('r.id', 0);
  96. } else {
  97. $query->whereIn('r.manage_agent_id', $managerIds);
  98. }
  99. }
  100. $source = trim((string) ($filters['source'] ?? ''));
  101. if ($source !== '') {
  102. if (!ctype_digit($source) || !isset(self::SOURCE_NAMES[(int) $source])) {
  103. throw new ApiException('用户来源不支持');
  104. }
  105. $query->where('r.source', (int) $source);
  106. }
  107. $cardType = trim((string) ($filters['card_type'] ?? ''));
  108. if ($cardType !== '') {
  109. if (!ctype_digit($cardType) || !isset(self::CARD_TYPE_NAMES[(int) $cardType])) {
  110. throw new ApiException('卡类型不支持');
  111. }
  112. $query->where('r.card_type', (int) $cardType);
  113. }
  114. $deviceType = trim((string) ($filters['device_type'] ?? ''));
  115. if ($deviceType !== '') {
  116. if (!ctype_digit($deviceType) || !isset(self::DEVICE_NAMES[(int) $deviceType])) {
  117. throw new ApiException('设备类型不支持');
  118. }
  119. $query->where('r.device_type', (int) $deviceType);
  120. }
  121. $userId = trim((string) ($filters['user_id'] ?? ''));
  122. if ($userId !== '') {
  123. if (!ctype_digit($userId) || (int) $userId <= 0) {
  124. throw new ApiException('用户 ID 格式不正确');
  125. }
  126. $query->where('r.user_id', (int) $userId);
  127. }
  128. $nickname = trim((string) ($filters['nickname'] ?? ''));
  129. if ($nickname !== '') {
  130. $query->whereLike('u.nickname', '%' . $nickname . '%');
  131. }
  132. [$startDate, $endDate] = $this->dateRange($filters);
  133. if ($startDate !== '') {
  134. $query->where('r.activation_time', '>=', $startDate . ' 00:00:00');
  135. }
  136. if ($endDate !== '') {
  137. $query->where('r.activation_time', '<=', $endDate . ' 23:59:59');
  138. }
  139. return $query;
  140. }
  141. private function summary(array $filters): array
  142. {
  143. $query = $this->buildQuery($filters);
  144. $query->removeOption('field');
  145. $rows = $query->field(implode(',', [
  146. 'COUNT(*) AS record_count',
  147. 'COALESCE(SUM(CASE WHEN r.status = 1 THEN r.pay_amount ELSE 0 END), 0) AS total_paid_amount',
  148. 'SUM(CASE WHEN r.status = 1 AND r.expire_time >= NOW() THEN 1 ELSE 0 END) AS active_count',
  149. 'SUM(CASE WHEN r.status = 1 AND r.expire_time < NOW() THEN 1 ELSE 0 END) AS expired_count',
  150. ]))->select()->toArray();
  151. $row = $rows[0] ?? [];
  152. return [
  153. 'record_count' => (int) ($row['record_count'] ?? 0),
  154. 'total_paid_amount' => round((float) ($row['total_paid_amount'] ?? 0), 2),
  155. 'active_count' => (int) ($row['active_count'] ?? 0),
  156. 'expired_count' => (int) ($row['expired_count'] ?? 0),
  157. ];
  158. }
  159. private function formatRecord(array $row): array
  160. {
  161. $source = (int) ($row['source'] ?? 0);
  162. $cardType = (int) ($row['card_type'] ?? -1);
  163. $deviceType = (int) ($row['device_type'] ?? 0);
  164. $status = (int) ($row['status'] ?? 0);
  165. $merchantName = trim((string) ($row['merchant_name_snapshot'] ?? ''));
  166. return [
  167. 'id' => (int) ($row['id'] ?? 0),
  168. 'merchant_order_sn' => (string) ($row['merchant_order_sn'] ?? ''),
  169. 'transaction_sn' => (string) ($row['transaction_sn'] ?? ''),
  170. 'user_id' => (int) ($row['user_id'] ?? 0),
  171. 'nickname' => (string) (($row['nickname'] ?? '') ?: '未设置昵称'),
  172. 'phone' => (string) ($row['phone'] ?? ''),
  173. 'avatar' => (string) ($row['avatar'] ?? ''),
  174. 'source' => $source,
  175. 'source_name' => self::SOURCE_NAMES[$source] ?? '其他',
  176. 'device_type' => $deviceType,
  177. 'device_type_name' => self::DEVICE_NAMES[$deviceType] ?? '其他设备',
  178. 'merchant_id' => (int) ($row['merchant_id'] ?? 0),
  179. 'merchant_name' => $merchantName !== '' ? $merchantName : (string) (($row['current_merchant_name'] ?? '') ?: '--'),
  180. 'pay_amount' => round((float) ($row['pay_amount'] ?? 0), 2),
  181. 'card_type' => $cardType,
  182. 'card_type_name' => self::CARD_TYPE_NAMES[$cardType] ?? '其他卡',
  183. 'activation_time' => (string) ($row['activation_time'] ?? ''),
  184. 'expire_time' => (string) ($row['expire_time'] ?? ''),
  185. 'status' => $status,
  186. 'status_name' => self::STATUS_NAMES[$status] ?? '未知状态',
  187. 'remark' => (string) ($row['remark'] ?? ''),
  188. ];
  189. }
  190. /** @return array{0:string,1:string} */
  191. private function dateRange(array $filters): array
  192. {
  193. $startDate = trim((string) ($filters['start_date'] ?? ''));
  194. $endDate = trim((string) ($filters['end_date'] ?? ''));
  195. foreach ([['开始', $startDate], ['结束', $endDate]] as [$label, $date]) {
  196. if ($date === '') {
  197. continue;
  198. }
  199. $parsed = DateTime::createFromFormat('!Y-m-d', $date);
  200. if (!$parsed || $parsed->format('Y-m-d') !== $date) {
  201. throw new ApiException($label . '日期格式不正确');
  202. }
  203. }
  204. if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
  205. throw new ApiException('开始日期不能晚于结束日期');
  206. }
  207. return [$startDate, $endDate];
  208. }
  209. /** @return int[]|null */
  210. private function accessibleManagerIds(): ?array
  211. {
  212. $currentId = (int) ($this->adminInfo['id'] ?? 0);
  213. if ($currentId === 1) {
  214. return null;
  215. }
  216. if ($currentId <= 0) {
  217. return [];
  218. }
  219. $managerIds = [$currentId];
  220. $dept = $this->adminInfo['deptList'] ?? [];
  221. if (empty($dept['id']) || !isset($dept['level'])) {
  222. return $managerIds;
  223. }
  224. $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
  225. $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
  226. ->whereLike('level', $prefix . '%')->column('id');
  227. if ($deptIds !== []) {
  228. $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
  229. ->whereNull('delete_time')->column('id')));
  230. }
  231. return array_values(array_unique($managerIds));
  232. }
  233. private function excelSafe(mixed $value): string
  234. {
  235. $value = (string) $value;
  236. return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
  237. }
  238. }