RechargeRecordLogic.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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 RechargeRecordLogic extends BaseLogic
  14. {
  15. private const BALANCE_TYPE_NAMES = [
  16. 'wallet' => '钱包充值',
  17. 'card' => '储值卡本金',
  18. 'card_gift' => '储值卡充值',
  19. ];
  20. private const EXPORT_LIMIT = 50000;
  21. public function indexList(array $filters): array
  22. {
  23. $page = max(1, (int) request()->input('page', 1));
  24. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  25. $result = $this->buildQuery($filters)
  26. ->order('l.created_at', 'desc')
  27. ->order('l.id', 'desc')
  28. ->paginate(['page' => $page, 'list_rows' => $limit])
  29. ->toArray();
  30. $result['data'] = array_map(fn (array $row): array => $this->formatRecord($row), $result['data'] ?? []);
  31. $result['summary'] = $this->summary($filters);
  32. return $result;
  33. }
  34. public function export(array $filters): Response
  35. {
  36. $query = $this->buildQuery($filters);
  37. $count = (int) (clone $query)->count();
  38. if ($count > self::EXPORT_LIMIT) {
  39. throw new ApiException('导出记录超过 50000 条,请缩小筛选范围');
  40. }
  41. $rows = $query->order('l.created_at', 'desc')->order('l.id', 'desc')->select()->toArray();
  42. $data = [];
  43. foreach ($rows as $row) {
  44. $record = $this->formatRecord($row);
  45. $data[] = [
  46. 'id' => $record['id'],
  47. 'user_id' => $record['user_id'],
  48. 'nickname' => $this->excelSafe($record['nickname']),
  49. 'phone' => $this->excelSafe($record['phone']),
  50. 'user_source_name' => $record['user_source_name'],
  51. 'balance_type_name' => $record['balance_type_name'],
  52. 'direction_name' => $record['direction_name'],
  53. 'change_amount' => $record['change_amount'],
  54. 'before_balance' => $record['before_balance'],
  55. 'after_balance' => $record['after_balance'],
  56. 'source_name' => $record['source_name'],
  57. 'operator_name' => $this->excelSafe($record['operator_name']),
  58. 'created_at' => $record['created_at'],
  59. 'remark' => $this->excelSafe($record['remark']),
  60. ];
  61. }
  62. $fileName = '充值记录_' . date('Ymd_His') . '.xlsx';
  63. $writer = new OpenSpoutWriter($fileName);
  64. $writer->setWidth([10, 12, 22, 18, 12, 18, 12, 14, 14, 14, 14, 18, 22, 36]);
  65. $writer->setHeader([
  66. '记录ID', '用户ID', '用户昵称', '手机号', '用户来源', '账户类型', '变动方向',
  67. '变动金额', '变动前余额', '变动后余额', '充值方式', '操作人', '充值时间', '备注',
  68. ]);
  69. $writer->setData($data);
  70. return response()->download($writer->returnFile(), urlencode($fileName));
  71. }
  72. private function buildQuery(array $filters)
  73. {
  74. $query = Db::table('sa_rental_user_wallet_log')->alias('l')
  75. ->leftJoin('sa_rental_user u', 'u.id = l.user_id AND u.delete_time IS NULL')
  76. ->field(implode(',', [
  77. 'l.id', 'l.user_id', 'l.balance_type', 'l.direction', 'l.amount',
  78. 'l.before_balance', 'l.after_balance', 'l.source', 'l.remark',
  79. 'l.operator_id', 'l.operator_name', 'l.created_at',
  80. 'u.nickname', 'u.phone', 'u.avatar', 'u.source AS user_source',
  81. ]));
  82. $managerIds = $this->accessibleManagerIds();
  83. if (is_array($managerIds)) {
  84. $userIds = $managerIds === [] ? [] : Db::table('sa_rental_order')
  85. ->whereNull('delete_time')
  86. ->whereIn('manage_agent_id', $managerIds)
  87. ->where('user_id', '>', 0)
  88. ->distinct(true)
  89. ->column('user_id');
  90. if ($userIds === []) {
  91. $query->where('l.id', 0);
  92. } else {
  93. $query->whereIn('l.user_id', array_map('intval', $userIds));
  94. }
  95. }
  96. $balanceType = trim((string) ($filters['balance_type'] ?? ''));
  97. if ($balanceType !== '') {
  98. if (!isset(self::BALANCE_TYPE_NAMES[$balanceType])) {
  99. throw new ApiException('充值类型不支持');
  100. }
  101. $query->where('l.balance_type', $balanceType);
  102. }
  103. $direction = trim((string) ($filters['direction'] ?? ''));
  104. if ($direction !== '') {
  105. if (!in_array($direction, ['1', '-1'], true)) {
  106. throw new ApiException('变动方向不支持');
  107. }
  108. $query->where('l.direction', (int) $direction);
  109. }
  110. $userId = trim((string) ($filters['user_id'] ?? ''));
  111. if ($userId !== '') {
  112. if (!ctype_digit($userId) || (int) $userId <= 0) {
  113. throw new ApiException('用户 ID 格式不正确');
  114. }
  115. $query->where('l.user_id', (int) $userId);
  116. }
  117. $nickname = trim((string) ($filters['nickname'] ?? ''));
  118. if ($nickname !== '') {
  119. $query->whereLike('u.nickname', '%' . $nickname . '%');
  120. }
  121. [$startDate, $endDate] = $this->dateRange($filters);
  122. if ($startDate !== '') {
  123. $query->where('l.created_at', '>=', $startDate . ' 00:00:00');
  124. }
  125. if ($endDate !== '') {
  126. $query->where('l.created_at', '<=', $endDate . ' 23:59:59');
  127. }
  128. return $query;
  129. }
  130. private function summary(array $filters): array
  131. {
  132. $query = $this->buildQuery($filters);
  133. $query->removeOption('field');
  134. $rows = $query->field(implode(',', [
  135. 'COUNT(*) AS record_count',
  136. 'COALESCE(SUM(CASE WHEN l.amount > 0 THEN l.amount ELSE 0 END), 0) AS increase_amount',
  137. 'COALESCE(SUM(CASE WHEN l.amount < 0 THEN ABS(l.amount) ELSE 0 END), 0) AS decrease_amount',
  138. 'COALESCE(SUM(l.amount), 0) AS net_amount',
  139. ]))->select()->toArray();
  140. $row = $rows[0] ?? [];
  141. return [
  142. 'record_count' => (int) ($row['record_count'] ?? 0),
  143. 'increase_amount' => round((float) ($row['increase_amount'] ?? 0), 2),
  144. 'decrease_amount' => round((float) ($row['decrease_amount'] ?? 0), 2),
  145. 'net_amount' => round((float) ($row['net_amount'] ?? 0), 2),
  146. ];
  147. }
  148. private function formatRecord(array $row): array
  149. {
  150. $balanceType = (string) ($row['balance_type'] ?? '');
  151. $amount = round((float) ($row['amount'] ?? 0), 2);
  152. $direction = (int) ($row['direction'] ?? ($amount >= 0 ? 1 : -1));
  153. $operatorId = (int) ($row['operator_id'] ?? 0);
  154. $operatorName = trim((string) ($row['operator_name'] ?? ''));
  155. $source = (string) ($row['source'] ?? '');
  156. return [
  157. 'id' => (int) $row['id'],
  158. 'user_id' => (int) ($row['user_id'] ?? 0),
  159. 'nickname' => (string) (($row['nickname'] ?? '') ?: '未设置昵称'),
  160. 'phone' => (string) ($row['phone'] ?? ''),
  161. 'avatar' => (string) ($row['avatar'] ?? ''),
  162. 'user_source' => (int) ($row['user_source'] ?? 0),
  163. 'user_source_name' => (int) ($row['user_source'] ?? 0) === 2 ? '支付宝' : '微信',
  164. 'balance_type' => $balanceType,
  165. 'balance_type_name' => self::BALANCE_TYPE_NAMES[$balanceType] ?? '其他账户',
  166. 'direction' => $direction,
  167. 'direction_name' => $direction === 1 ? '增加' : '扣减',
  168. 'amount' => $amount,
  169. 'change_amount' => abs($amount),
  170. 'before_balance' => round((float) ($row['before_balance'] ?? 0), 2),
  171. 'after_balance' => round((float) ($row['after_balance'] ?? 0), 2),
  172. 'source' => $source,
  173. 'source_name' => $source === 'admin_recharge' ? '后台调整' : ($source ?: '其他'),
  174. 'remark' => (string) ($row['remark'] ?? ''),
  175. 'operator_id' => $operatorId,
  176. 'operator_name' => $operatorName !== '' ? $operatorName : '管理员#' . $operatorId,
  177. 'created_at' => (string) ($row['created_at'] ?? ''),
  178. 'status_name' => '已完成',
  179. ];
  180. }
  181. /** @return array{0:string,1:string} */
  182. private function dateRange(array $filters): array
  183. {
  184. $startDate = trim((string) ($filters['start_date'] ?? ''));
  185. $endDate = trim((string) ($filters['end_date'] ?? ''));
  186. foreach ([['开始', $startDate], ['结束', $endDate]] as [$label, $date]) {
  187. if ($date === '') {
  188. continue;
  189. }
  190. $parsed = DateTime::createFromFormat('!Y-m-d', $date);
  191. if (!$parsed || $parsed->format('Y-m-d') !== $date) {
  192. throw new ApiException($label . '日期格式不正确');
  193. }
  194. }
  195. if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
  196. throw new ApiException('开始日期不能晚于结束日期');
  197. }
  198. return [$startDate, $endDate];
  199. }
  200. /** @return int[]|null */
  201. private function accessibleManagerIds(): ?array
  202. {
  203. $currentId = (int) ($this->adminInfo['id'] ?? 0);
  204. if ($currentId === 1) {
  205. return null;
  206. }
  207. if ($currentId <= 0) {
  208. return [];
  209. }
  210. $managerIds = [$currentId];
  211. $dept = $this->adminInfo['deptList'] ?? [];
  212. if (empty($dept['id']) || !isset($dept['level'])) {
  213. return $managerIds;
  214. }
  215. $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
  216. $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
  217. ->whereLike('level', $prefix . '%')->column('id');
  218. if ($deptIds !== []) {
  219. $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
  220. ->whereNull('delete_time')->column('id')));
  221. }
  222. return array_values(array_unique($managerIds));
  223. }
  224. private function excelSafe(mixed $value): string
  225. {
  226. $value = (string) $value;
  227. return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
  228. }
  229. }