|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use App\Models\Blacklist;
|
|
|
use App\Models\Reservation;
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
|
use Illuminate\Support\Collection;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
class BlacklistController extends Controller
|
|
|
{
|
|
|
public function users(Request $request): JsonResponse
|
|
|
{
|
|
|
$user = $request->user();
|
|
|
$allowedVenueIds = $this->allowedVenueIds($request);
|
|
|
$pageSize = max(1, min(100, (int) $request->input('page_size', 10)));
|
|
|
|
|
|
if ($user->isSuperAdmin() && Schema::hasTable('wechat_users')) {
|
|
|
$query = DB::table('wechat_users as wu')
|
|
|
->leftJoin('reservations as r', 'r.wechat_user_id', '=', 'wu.id')
|
|
|
->when($request->filled('venue_id'), fn ($q) => $q->where('r.venue_id', (int) $request->input('venue_id')))
|
|
|
->when($request->filled('keyword'), function ($q) use ($request) {
|
|
|
$keyword = trim((string) $request->input('keyword'));
|
|
|
$q->where(function ($w) use ($keyword) {
|
|
|
$w->where('wu.nickname', 'like', "%{$keyword}%")
|
|
|
->orWhere('wu.openid', 'like', "%{$keyword}%")
|
|
|
->orWhere('wu.unionid', 'like', "%{$keyword}%")
|
|
|
->orWhere('r.visitor_phone', 'like', "%{$keyword}%")
|
|
|
->orWhere('r.visitor_name', 'like', "%{$keyword}%")
|
|
|
->orWhere('r.id_card', 'like', "%{$keyword}%");
|
|
|
});
|
|
|
})
|
|
|
->selectRaw('wu.id as wechat_user_id, wu.openid, wu.unionid, wu.nickname, MAX(r.id) as latest_id, COUNT(r.id) as reservation_count, MAX(r.created_at) as last_reserved_at')
|
|
|
->groupBy('wu.id', 'wu.openid', 'wu.unionid', 'wu.nickname')
|
|
|
->orderByDesc('wu.id');
|
|
|
|
|
|
$base = $query->paginate($pageSize);
|
|
|
$latestIds = collect($base->items())->pluck('latest_id')->filter()->map(fn ($v) => (int) $v)->values();
|
|
|
$wechatIds = collect($base->items())->pluck('wechat_user_id')->filter()->map(fn ($v) => (int) $v)->values();
|
|
|
|
|
|
$latestRows = Reservation::query()
|
|
|
->whereIn('id', $latestIds)
|
|
|
->get(['id', 'wechat_user_id', 'visitor_phone', 'visitor_name', 'id_card'])
|
|
|
->keyBy('id');
|
|
|
|
|
|
$venueRows = Reservation::query()
|
|
|
->join('venues', 'venues.id', '=', 'reservations.venue_id')
|
|
|
->whereIn('reservations.wechat_user_id', $wechatIds)
|
|
|
->selectRaw('reservations.wechat_user_id, reservations.venue_id, venues.name')
|
|
|
->groupBy('reservations.wechat_user_id', 'reservations.venue_id', 'venues.name')
|
|
|
->get()
|
|
|
->groupBy('wechat_user_id');
|
|
|
|
|
|
$phones = $latestRows->pluck('visitor_phone')->filter()->values();
|
|
|
$blackMapByPhone = Blacklist::query()
|
|
|
->join('venues', 'venues.id', '=', 'blacklists.venue_id')
|
|
|
->whereIn('blacklists.visitor_phone', $phones)
|
|
|
->selectRaw('blacklists.visitor_phone, blacklists.venue_id, venues.name, blacklists.reason')
|
|
|
->get()
|
|
|
->groupBy('visitor_phone');
|
|
|
|
|
|
$base->setCollection(collect($base->items())->map(function ($row) use ($latestRows, $venueRows, $blackMapByPhone) {
|
|
|
$latest = $row->latest_id ? $latestRows->get((int) $row->latest_id) : null;
|
|
|
$currentVenues = ($venueRows->get((int) $row->wechat_user_id) ?? collect())
|
|
|
->map(fn ($v) => ['id' => (int) $v->venue_id, 'name' => (string) $v->name])
|
|
|
->values();
|
|
|
$blacklistedVenues = $latest?->visitor_phone
|
|
|
? ($blackMapByPhone->get($latest->visitor_phone) ?? collect())
|
|
|
->map(fn ($v) => ['id' => (int) $v->venue_id, 'name' => (string) $v->name, 'reason' => $v->reason])
|
|
|
->values()
|
|
|
: collect();
|
|
|
|
|
|
return [
|
|
|
'user_key' => 'wu-'.$row->wechat_user_id,
|
|
|
'visitor_phone' => $latest?->visitor_phone,
|
|
|
'visitor_name' => $latest?->visitor_name ?: $row->nickname,
|
|
|
'id_card' => $latest?->id_card,
|
|
|
'openid' => $row->openid,
|
|
|
'unionid' => $row->unionid,
|
|
|
'reservation_count' => (int) $row->reservation_count,
|
|
|
'last_reserved_at' => $row->last_reserved_at,
|
|
|
'venues' => $currentVenues,
|
|
|
'blacklisted_venues' => $blacklistedVenues,
|
|
|
'disabled' => !$latest?->visitor_phone,
|
|
|
];
|
|
|
}));
|
|
|
|
|
|
return response()->json($base);
|
|
|
}
|
|
|
|
|
|
$query = Reservation::query()
|
|
|
->whereNotNull('visitor_phone')
|
|
|
->where('visitor_phone', '!=', '');
|
|
|
|
|
|
if (!$user->isSuperAdmin()) {
|
|
|
$query->whereIn('venue_id', $allowedVenueIds);
|
|
|
} elseif ($request->filled('venue_id')) {
|
|
|
$query->where('venue_id', (int) $request->input('venue_id'));
|
|
|
}
|
|
|
|
|
|
if ($request->filled('keyword')) {
|
|
|
$keyword = trim((string) $request->input('keyword'));
|
|
|
$query->where(function ($q) use ($keyword) {
|
|
|
$q->where('visitor_phone', 'like', "%{$keyword}%")
|
|
|
->orWhere('visitor_name', 'like', "%{$keyword}%")
|
|
|
->orWhere('id_card', 'like', "%{$keyword}%");
|
|
|
});
|
|
|
}
|
|
|
|
|
|
$base = (clone $query)
|
|
|
->selectRaw('visitor_phone, MAX(id) as latest_id, COUNT(*) as reservation_count, MAX(created_at) as last_reserved_at')
|
|
|
->groupBy('visitor_phone')
|
|
|
->orderByDesc('latest_id')
|
|
|
->paginate($pageSize);
|
|
|
|
|
|
$phones = collect($base->items())->pluck('visitor_phone')->filter()->values();
|
|
|
if ($phones->isEmpty()) {
|
|
|
return response()->json($base);
|
|
|
}
|
|
|
|
|
|
$latestRows = Reservation::query()
|
|
|
->whereIn('id', collect($base->items())->pluck('latest_id'))
|
|
|
->get(['id', 'visitor_phone', 'visitor_name', 'id_card'])
|
|
|
->keyBy('id');
|
|
|
|
|
|
$venueRows = Reservation::query()
|
|
|
->join('venues', 'venues.id', '=', 'reservations.venue_id')
|
|
|
->whereIn('reservations.visitor_phone', $phones)
|
|
|
->when(!$user->isSuperAdmin(), fn ($q) => $q->whereIn('reservations.venue_id', $allowedVenueIds))
|
|
|
->selectRaw('reservations.visitor_phone, reservations.venue_id, venues.name')
|
|
|
->groupBy('reservations.visitor_phone', 'reservations.venue_id', 'venues.name')
|
|
|
->get();
|
|
|
|
|
|
$blackRows = Blacklist::query()
|
|
|
->join('venues', 'venues.id', '=', 'blacklists.venue_id')
|
|
|
->whereIn('blacklists.visitor_phone', $phones)
|
|
|
->when(!$user->isSuperAdmin(), fn ($q) => $q->whereIn('blacklists.venue_id', $allowedVenueIds))
|
|
|
->selectRaw('blacklists.visitor_phone, blacklists.venue_id, venues.name, blacklists.reason')
|
|
|
->get();
|
|
|
|
|
|
$venueMap = $venueRows->groupBy('visitor_phone');
|
|
|
$blackMap = $blackRows->groupBy('visitor_phone');
|
|
|
|
|
|
$base->setCollection(collect($base->items())->map(function ($row) use ($latestRows, $venueMap, $blackMap) {
|
|
|
$latest = $latestRows->get((int) $row->latest_id);
|
|
|
$venues = ($venueMap->get($row->visitor_phone) ?? collect())
|
|
|
->map(fn ($v) => ['id' => (int) $v->venue_id, 'name' => (string) $v->name])
|
|
|
->values();
|
|
|
$blacklistedVenues = ($blackMap->get($row->visitor_phone) ?? collect())
|
|
|
->map(fn ($v) => ['id' => (int) $v->venue_id, 'name' => (string) $v->name, 'reason' => $v->reason])
|
|
|
->values();
|
|
|
|
|
|
return [
|
|
|
'user_key' => 'phone-'.$row->visitor_phone,
|
|
|
'visitor_phone' => $row->visitor_phone,
|
|
|
'visitor_name' => $latest?->visitor_name,
|
|
|
'id_card' => $latest?->id_card,
|
|
|
'openid' => null,
|
|
|
'unionid' => null,
|
|
|
'reservation_count' => (int) $row->reservation_count,
|
|
|
'last_reserved_at' => $row->last_reserved_at,
|
|
|
'venues' => $venues,
|
|
|
'blacklisted_venues' => $blacklistedVenues,
|
|
|
'disabled' => false,
|
|
|
];
|
|
|
}));
|
|
|
|
|
|
return response()->json($base);
|
|
|
}
|
|
|
|
|
|
public function batchBlacklist(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'phones' => ['required', 'array', 'min:1'],
|
|
|
'phones.*' => ['required', 'string', 'max:20'],
|
|
|
'venue_ids' => ['required', 'array', 'min:1'],
|
|
|
'venue_ids.*' => ['required', 'integer', 'exists:venues,id'],
|
|
|
'reason' => ['required', 'string', 'max:255'],
|
|
|
]);
|
|
|
|
|
|
$allowedVenueIds = $this->allowedVenueIds($request);
|
|
|
$venueIds = collect($data['venue_ids'])->map(fn ($v) => (int) $v)->unique()->values();
|
|
|
foreach ($venueIds as $venueId) {
|
|
|
$this->ensureVenuePermission($request, $venueId);
|
|
|
}
|
|
|
|
|
|
$phones = collect($data['phones'])->map(fn ($v) => trim((string) $v))->filter()->unique()->values();
|
|
|
$result = $this->applyBlacklistAction($phones, $venueIds, (string) $data['reason'], $allowedVenueIds, true);
|
|
|
|
|
|
return response()->json(array_merge(['message' => '批量拉黑完成'], $result));
|
|
|
}
|
|
|
|
|
|
public function batchUnblacklist(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'phones' => ['required', 'array', 'min:1'],
|
|
|
'phones.*' => ['required', 'string', 'max:20'],
|
|
|
'venue_ids' => ['required', 'array', 'min:1'],
|
|
|
'venue_ids.*' => ['required', 'integer', 'exists:venues,id'],
|
|
|
'reason' => ['required', 'string', 'max:255'],
|
|
|
]);
|
|
|
|
|
|
$allowedVenueIds = $this->allowedVenueIds($request);
|
|
|
$venueIds = collect($data['venue_ids'])->map(fn ($v) => (int) $v)->unique()->values();
|
|
|
foreach ($venueIds as $venueId) {
|
|
|
$this->ensureVenuePermission($request, $venueId);
|
|
|
}
|
|
|
|
|
|
$phones = collect($data['phones'])->map(fn ($v) => trim((string) $v))->filter()->unique()->values();
|
|
|
$result = $this->applyBlacklistAction($phones, $venueIds, (string) $data['reason'], $allowedVenueIds, false);
|
|
|
|
|
|
return response()->json(array_merge(['message' => '批量解除黑名单完成'], $result));
|
|
|
}
|
|
|
|
|
|
public function index(Request $request): JsonResponse
|
|
|
{
|
|
|
$query = Blacklist::with('venue:id,name')->orderByDesc('id');
|
|
|
$this->restrictByVenue($request, $query);
|
|
|
if ($request->filled('keyword')) {
|
|
|
$keyword = trim((string) $request->input('keyword'));
|
|
|
$query->where(function ($q) use ($keyword) {
|
|
|
$q->where('visitor_phone', 'like', "%{$keyword}%")
|
|
|
->orWhere('visitor_name', 'like', "%{$keyword}%");
|
|
|
});
|
|
|
}
|
|
|
return response()->json($query->get());
|
|
|
}
|
|
|
|
|
|
public function store(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'venue_id' => ['required', 'integer', 'exists:venues,id'],
|
|
|
'visitor_name' => ['nullable', 'string', 'max:100'],
|
|
|
'visitor_phone' => ['required', 'string', 'max:20'],
|
|
|
'reason' => ['nullable', 'string', 'max:255'],
|
|
|
]);
|
|
|
|
|
|
$this->ensureVenuePermission($request, (int) $data['venue_id']);
|
|
|
|
|
|
$item = Blacklist::updateOrCreate(
|
|
|
['venue_id' => $data['venue_id'], 'visitor_phone' => $data['visitor_phone']],
|
|
|
['visitor_name' => $data['visitor_name'] ?? null, 'reason' => $data['reason'] ?? null],
|
|
|
);
|
|
|
|
|
|
return response()->json($item->load('venue:id,name'), 201);
|
|
|
}
|
|
|
|
|
|
public function destroy(Request $request, Blacklist $blacklist): JsonResponse
|
|
|
{
|
|
|
$this->ensureVenuePermission($request, $blacklist->venue_id);
|
|
|
$blacklist->delete();
|
|
|
return response()->json(['message' => '删除成功']);
|
|
|
}
|
|
|
|
|
|
public function update(Request $request, Blacklist $blacklist): JsonResponse
|
|
|
{
|
|
|
$this->ensureVenuePermission($request, $blacklist->venue_id);
|
|
|
$data = $request->validate([
|
|
|
'visitor_name' => ['nullable', 'string', 'max:100'],
|
|
|
'reason' => ['nullable', 'string', 'max:255'],
|
|
|
]);
|
|
|
$blacklist->fill($data)->save();
|
|
|
return response()->json($blacklist->fresh()->load('venue:id,name'));
|
|
|
}
|
|
|
|
|
|
public function batchImport(Request $request): JsonResponse
|
|
|
{
|
|
|
$data = $request->validate([
|
|
|
'venue_id' => ['required', 'integer', 'exists:venues,id'],
|
|
|
'phones' => ['required', 'string'],
|
|
|
'reason' => ['nullable', 'string', 'max:255'],
|
|
|
'dedupe_mode' => ['nullable', 'in:overwrite,keep'],
|
|
|
]);
|
|
|
$this->ensureVenuePermission($request, (int) $data['venue_id']);
|
|
|
|
|
|
$phones = preg_split('/[\s,;,;]+/u', $data['phones']) ?: [];
|
|
|
$phones = array_values(array_unique(array_filter(array_map('trim', $phones))));
|
|
|
$created = 0;
|
|
|
$failed = [];
|
|
|
$dedupeMode = $data['dedupe_mode'] ?? 'overwrite';
|
|
|
foreach ($phones as $phone) {
|
|
|
if ($phone === '') {
|
|
|
continue;
|
|
|
}
|
|
|
if (!preg_match('/^1\d{10}$/', $phone)) {
|
|
|
$failed[] = ['phone' => $phone, 'reason' => '手机号格式不正确'];
|
|
|
continue;
|
|
|
}
|
|
|
$existing = Blacklist::where('venue_id', (int) $data['venue_id'])
|
|
|
->where('visitor_phone', $phone)
|
|
|
->first();
|
|
|
if ($existing) {
|
|
|
if ($dedupeMode === 'overwrite') {
|
|
|
$existing->reason = $data['reason'] ?? $existing->reason ?? '批量导入';
|
|
|
$existing->save();
|
|
|
}
|
|
|
} else {
|
|
|
Blacklist::create([
|
|
|
'venue_id' => (int) $data['venue_id'],
|
|
|
'visitor_phone' => $phone,
|
|
|
'reason' => $data['reason'] ?? '批量导入',
|
|
|
]);
|
|
|
$created++;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return response()->json([
|
|
|
'message' => '批量导入完成',
|
|
|
'count' => $created,
|
|
|
'failed_count' => count($failed),
|
|
|
'failed' => $failed,
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
private function restrictByVenue(Request $request, $query): void
|
|
|
{
|
|
|
$user = $request->user();
|
|
|
if ($user->isSuperAdmin()) {
|
|
|
return;
|
|
|
}
|
|
|
$query->whereIn('venue_id', $user->venues()->pluck('venues.id'));
|
|
|
}
|
|
|
|
|
|
private function ensureVenuePermission(Request $request, int $venueId): void
|
|
|
{
|
|
|
$user = $request->user();
|
|
|
if ($user->isSuperAdmin()) {
|
|
|
return;
|
|
|
}
|
|
|
$allowed = $user->venues()->where('venues.id', $venueId)->exists();
|
|
|
abort_unless($allowed, 403, '仅可操作已绑定场馆');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param Collection<int, string> $phones
|
|
|
* @param Collection<int, int> $venueIds
|
|
|
* @param Collection<int, int> $allowedVenueIds
|
|
|
* @return array<string, mixed>
|
|
|
*/
|
|
|
private function applyBlacklistAction(
|
|
|
Collection $phones,
|
|
|
Collection $venueIds,
|
|
|
string $reason,
|
|
|
Collection $allowedVenueIds,
|
|
|
bool $toBlacklist
|
|
|
): array {
|
|
|
$pairs = Reservation::query()
|
|
|
->whereIn('visitor_phone', $phones)
|
|
|
->whereIn('venue_id', $venueIds)
|
|
|
->select(['visitor_phone', 'venue_id'])
|
|
|
->groupBy('visitor_phone', 'venue_id')
|
|
|
->get()
|
|
|
->map(fn ($r) => $r->visitor_phone.'#'.$r->venue_id)
|
|
|
->values()
|
|
|
->all();
|
|
|
$pairSet = array_fill_keys($pairs, true);
|
|
|
|
|
|
$affected = 0;
|
|
|
$skipped = 0;
|
|
|
$failed = [];
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
try {
|
|
|
foreach ($phones as $phone) {
|
|
|
foreach ($venueIds as $venueId) {
|
|
|
if ($allowedVenueIds->isNotEmpty() && !$allowedVenueIds->contains($venueId)) {
|
|
|
$failed[] = ['phone' => $phone, 'venue_id' => $venueId, 'reason' => '无场馆权限'];
|
|
|
continue;
|
|
|
}
|
|
|
$key = $phone.'#'.$venueId;
|
|
|
if (!isset($pairSet[$key])) {
|
|
|
$failed[] = ['phone' => $phone, 'venue_id' => $venueId, 'reason' => '用户未预约该场馆'];
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
if ($toBlacklist) {
|
|
|
Blacklist::updateOrCreate(
|
|
|
['venue_id' => $venueId, 'visitor_phone' => $phone],
|
|
|
['reason' => $reason]
|
|
|
);
|
|
|
$affected++;
|
|
|
} else {
|
|
|
$deleted = Blacklist::where('venue_id', $venueId)->where('visitor_phone', $phone)->delete();
|
|
|
if ($deleted > 0) {
|
|
|
$affected += $deleted;
|
|
|
} else {
|
|
|
$skipped++;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
DB::commit();
|
|
|
} catch (\Throwable $e) {
|
|
|
DB::rollBack();
|
|
|
throw $e;
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'count' => $affected,
|
|
|
'skipped_count' => $skipped,
|
|
|
'failed_count' => count($failed),
|
|
|
'failed' => $failed,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return Collection<int, int>
|
|
|
*/
|
|
|
private function allowedVenueIds(Request $request): Collection
|
|
|
{
|
|
|
$user = $request->user();
|
|
|
if ($user->isSuperAdmin()) {
|
|
|
return collect();
|
|
|
}
|
|
|
|
|
|
return $user->venues()->pluck('venues.id')->map(fn ($id) => (int) $id)->values();
|
|
|
}
|
|
|
}
|