'钱包充值', 'card' => '储值卡本金', 'card_gift' => '储值卡充值', ]; private const EXPORT_LIMIT = 50000; public function indexList(array $filters): array { $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 20))); $result = $this->buildQuery($filters) ->order('l.created_at', 'desc') ->order('l.id', 'desc') ->paginate(['page' => $page, 'list_rows' => $limit]) ->toArray(); $result['data'] = array_map(fn (array $row): array => $this->formatRecord($row), $result['data'] ?? []); $result['summary'] = $this->summary($filters); return $result; } public function export(array $filters): Response { $query = $this->buildQuery($filters); $count = (int) (clone $query)->count(); if ($count > self::EXPORT_LIMIT) { throw new ApiException('导出记录超过 50000 条,请缩小筛选范围'); } $rows = $query->order('l.created_at', 'desc')->order('l.id', 'desc')->select()->toArray(); $data = []; foreach ($rows as $row) { $record = $this->formatRecord($row); $data[] = [ 'id' => $record['id'], 'user_id' => $record['user_id'], 'nickname' => $this->excelSafe($record['nickname']), 'phone' => $this->excelSafe($record['phone']), 'user_source_name' => $record['user_source_name'], 'balance_type_name' => $record['balance_type_name'], 'direction_name' => $record['direction_name'], 'change_amount' => $record['change_amount'], 'before_balance' => $record['before_balance'], 'after_balance' => $record['after_balance'], 'source_name' => $record['source_name'], 'operator_name' => $this->excelSafe($record['operator_name']), 'created_at' => $record['created_at'], 'remark' => $this->excelSafe($record['remark']), ]; } $fileName = '充值记录_' . date('Ymd_His') . '.xlsx'; $writer = new OpenSpoutWriter($fileName); $writer->setWidth([10, 12, 22, 18, 12, 18, 12, 14, 14, 14, 14, 18, 22, 36]); $writer->setHeader([ '记录ID', '用户ID', '用户昵称', '手机号', '用户来源', '账户类型', '变动方向', '变动金额', '变动前余额', '变动后余额', '充值方式', '操作人', '充值时间', '备注', ]); $writer->setData($data); return response()->download($writer->returnFile(), urlencode($fileName)); } private function buildQuery(array $filters) { $query = Db::table('sa_rental_user_wallet_log')->alias('l') ->leftJoin('sa_rental_user u', 'u.id = l.user_id AND u.delete_time IS NULL') ->field(implode(',', [ 'l.id', 'l.user_id', 'l.balance_type', 'l.direction', 'l.amount', 'l.before_balance', 'l.after_balance', 'l.source', 'l.remark', 'l.operator_id', 'l.operator_name', 'l.created_at', 'u.nickname', 'u.phone', 'u.avatar', 'u.source AS user_source', ])); $managerIds = $this->accessibleManagerIds(); if (is_array($managerIds)) { $userIds = $managerIds === [] ? [] : Db::table('sa_rental_order') ->whereNull('delete_time') ->whereIn('manage_agent_id', $managerIds) ->where('user_id', '>', 0) ->distinct(true) ->column('user_id'); if ($userIds === []) { $query->where('l.id', 0); } else { $query->whereIn('l.user_id', array_map('intval', $userIds)); } } $balanceType = trim((string) ($filters['balance_type'] ?? '')); if ($balanceType !== '') { if (!isset(self::BALANCE_TYPE_NAMES[$balanceType])) { throw new ApiException('充值类型不支持'); } $query->where('l.balance_type', $balanceType); } $direction = trim((string) ($filters['direction'] ?? '')); if ($direction !== '') { if (!in_array($direction, ['1', '-1'], true)) { throw new ApiException('变动方向不支持'); } $query->where('l.direction', (int) $direction); } $userId = trim((string) ($filters['user_id'] ?? '')); if ($userId !== '') { if (!ctype_digit($userId) || (int) $userId <= 0) { throw new ApiException('用户 ID 格式不正确'); } $query->where('l.user_id', (int) $userId); } $nickname = trim((string) ($filters['nickname'] ?? '')); if ($nickname !== '') { $query->whereLike('u.nickname', '%' . $nickname . '%'); } [$startDate, $endDate] = $this->dateRange($filters); if ($startDate !== '') { $query->where('l.created_at', '>=', $startDate . ' 00:00:00'); } if ($endDate !== '') { $query->where('l.created_at', '<=', $endDate . ' 23:59:59'); } return $query; } private function summary(array $filters): array { $query = $this->buildQuery($filters); $query->removeOption('field'); $rows = $query->field(implode(',', [ 'COUNT(*) AS record_count', 'COALESCE(SUM(CASE WHEN l.amount > 0 THEN l.amount ELSE 0 END), 0) AS increase_amount', 'COALESCE(SUM(CASE WHEN l.amount < 0 THEN ABS(l.amount) ELSE 0 END), 0) AS decrease_amount', 'COALESCE(SUM(l.amount), 0) AS net_amount', ]))->select()->toArray(); $row = $rows[0] ?? []; return [ 'record_count' => (int) ($row['record_count'] ?? 0), 'increase_amount' => round((float) ($row['increase_amount'] ?? 0), 2), 'decrease_amount' => round((float) ($row['decrease_amount'] ?? 0), 2), 'net_amount' => round((float) ($row['net_amount'] ?? 0), 2), ]; } private function formatRecord(array $row): array { $balanceType = (string) ($row['balance_type'] ?? ''); $amount = round((float) ($row['amount'] ?? 0), 2); $direction = (int) ($row['direction'] ?? ($amount >= 0 ? 1 : -1)); $operatorId = (int) ($row['operator_id'] ?? 0); $operatorName = trim((string) ($row['operator_name'] ?? '')); $source = (string) ($row['source'] ?? ''); return [ 'id' => (int) $row['id'], 'user_id' => (int) ($row['user_id'] ?? 0), 'nickname' => (string) (($row['nickname'] ?? '') ?: '未设置昵称'), 'phone' => (string) ($row['phone'] ?? ''), 'avatar' => (string) ($row['avatar'] ?? ''), 'user_source' => (int) ($row['user_source'] ?? 0), 'user_source_name' => (int) ($row['user_source'] ?? 0) === 2 ? '支付宝' : '微信', 'balance_type' => $balanceType, 'balance_type_name' => self::BALANCE_TYPE_NAMES[$balanceType] ?? '其他账户', 'direction' => $direction, 'direction_name' => $direction === 1 ? '增加' : '扣减', 'amount' => $amount, 'change_amount' => abs($amount), 'before_balance' => round((float) ($row['before_balance'] ?? 0), 2), 'after_balance' => round((float) ($row['after_balance'] ?? 0), 2), 'source' => $source, 'source_name' => $source === 'admin_recharge' ? '后台调整' : ($source ?: '其他'), 'remark' => (string) ($row['remark'] ?? ''), 'operator_id' => $operatorId, 'operator_name' => $operatorName !== '' ? $operatorName : '管理员#' . $operatorId, 'created_at' => (string) ($row['created_at'] ?? ''), 'status_name' => '已完成', ]; } /** @return array{0:string,1:string} */ private function dateRange(array $filters): array { $startDate = trim((string) ($filters['start_date'] ?? '')); $endDate = trim((string) ($filters['end_date'] ?? '')); foreach ([['开始', $startDate], ['结束', $endDate]] as [$label, $date]) { if ($date === '') { continue; } $parsed = DateTime::createFromFormat('!Y-m-d', $date); if (!$parsed || $parsed->format('Y-m-d') !== $date) { throw new ApiException($label . '日期格式不正确'); } } if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) { throw new ApiException('开始日期不能晚于结束日期'); } return [$startDate, $endDate]; } /** @return int[]|null */ private function accessibleManagerIds(): ?array { $currentId = (int) ($this->adminInfo['id'] ?? 0); if ($currentId === 1) { return null; } if ($currentId <= 0) { return []; } $managerIds = [$currentId]; $dept = $this->adminInfo['deptList'] ?? []; if (empty($dept['id']) || !isset($dept['level'])) { return $managerIds; } $prefix = (string) $dept['level'] . (int) $dept['id'] . ','; $deptIds = Db::table('sa_system_dept')->whereNull('delete_time') ->whereLike('level', $prefix . '%')->column('id'); if ($deptIds !== []) { $managerIds = array_merge($managerIds, array_map('intval', SystemUser::whereIn('dept_id', $deptIds) ->whereNull('delete_time')->column('id'))); } return array_values(array_unique($managerIds)); } private function excelSafe(mixed $value): string { $value = (string) $value; return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value; } }