Files
chatroom/app/Http/Controllers/Admin/ForbiddenUsernameController.php
lkddi 477bba3003 修复:批量添加禁用词 UTF-8 编码错误
- 新增 sanitizeUtf8() 私有方法:
  去除 BOM、零宽字符、非法 UTF-8 字节、控制字符
  防止从聊天/文档复制时混入隐藏字符导致 json_encode 失败
- batchStore() 先净化 words 和 reason 输入
- preg_split 加 /u 修饰符(完整 Unicode 支持)
- 响应加 JSON_UNESCAPED_UNICODE 防中文被转义
2026-03-01 14:10:13 +08:00

205 lines
6.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能:禁用用户名管理控制器(站长专用)
*
* 管理 username_blacklist 表中 type=permanent 的永久禁用词列表。
* 包含:国家领导人名称、攻击性词汇、违禁词等不允许注册或改名的词语。
*
* 用户在注册AuthController和改名ShopService::useRenameCard时均会经过该表检测。
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\UsernameBlacklist;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class ForbiddenUsernameController extends Controller
{
/**
* 分页列出所有永久禁用词。
* 支持关键词模糊搜索GET ?q=xxx
*/
public function index(Request $request): \Illuminate\View\View
{
$q = $request->query('q', '');
$items = UsernameBlacklist::permanent()
->when($q, fn ($query) => $query->where('username', 'like', "%{$q}%"))
->orderByDesc('created_at')
->paginate(20)
->withQueryString();
return view('admin.forbidden-usernames.index', [
'items' => $items,
'q' => $q,
]);
}
/**
* 新增一条永久禁用词。
*
* @param Request $request 请求体username必填reason选填
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'username' => ['required', 'string', 'max:50'],
'reason' => ['nullable', 'string', 'max:100'],
], [
'username.required' => '禁用词不能为空。',
'username.max' => '禁用词最长50字符。',
]);
$username = trim($validated['username']);
// 已存在同名的永久记录则不重复插入
$exists = UsernameBlacklist::permanent()
->where('username', $username)
->exists();
if ($exists) {
return response()->json(['status' => 'error', 'message' => '该词语已在永久禁用列表中。'], 422);
}
UsernameBlacklist::create([
'username' => $username,
'type' => 'permanent',
'reserved_until' => null,
'reason' => $validated['reason'] ?? null,
'created_at' => Carbon::now(),
]);
return response()->json(['status' => 'success', 'message' => "{$username}」已加入永久禁用列表。"]);
}
/**
* 批量添加永久禁用词。
*
* 接受多行文本或逗号分隔的词语列表,自动去重并过滤已存在者。
* 返回成功添加数量和跳过数量。
*
* @param Request $request 请求体words换行/逗号分隔reason选填共用
*/
public function batchStore(Request $request): JsonResponse
{
$validated = $request->validate([
'words' => ['required', 'string'],
'reason' => ['nullable', 'string', 'max:100'],
], [
'words.required' => '请输入至少一个词语。',
]);
// 净化输入:去除非法 UTF-8 字节零宽字符、BOM、控制字符等防止 json_encode 失败
$rawInput = $this->sanitizeUtf8($validated['words']);
$reason = $this->sanitizeUtf8(trim($validated['reason'] ?? ''));
// 支持换行、逗号、中文逗号、空格分隔
$rawWords = preg_split('/[\r\n,\s]+/u', $rawInput);
// 过滤空串、超长词、去重
$words = collect($rawWords)
->map(fn ($w) => trim($w))
->filter(fn ($w) => $w !== '' && mb_strlen($w) <= 50)
->unique()
->values();
if ($words->isEmpty()) {
return response()->json(['status' => 'error', 'message' => '没有有效的词语,请检查输入。'], 422);
}
// 批量查询已存在的词(一次查询)
$existing = UsernameBlacklist::permanent()
->whereIn('username', $words->all())
->pluck('username')
->flip();
$now = Carbon::now();
$added = 0;
$rows = [];
foreach ($words as $word) {
if ($existing->has($word)) {
continue;
}
$rows[] = [
'username' => $word,
'type' => 'permanent',
'reserved_until' => null,
'reason' => $reason ?: null,
'created_at' => $now,
];
$added++;
}
if (! empty($rows)) {
UsernameBlacklist::insert($rows);
}
$skipped = $words->count() - $added;
$msg = "成功添加 {$added} 个词语".($skipped > 0 ? ",跳过 {$skipped} 个(已存在)" : '').'。';
return response()->json(['status' => 'success', 'message' => $msg, 'added' => $added], 200, [], JSON_UNESCAPED_UNICODE);
}
/**
* 净化字符串,移除非法 UTF-8 字节及常见控制/零宽字符。
*
* @param string $str 待净化字符串
* @return string 合法的 UTF-8 字符串
*/
private function sanitizeUtf8(string $str): string
{
// 去除 BOM
$str = str_replace("\xEF\xBB\xBF", '', $str);
// 去除零宽字符(零宽空格、零宽不连字等)
$str = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}\x{00AD}]/u', '', $str);
// 转换为合法 UTF-8忽略非法字节
$str = mb_convert_encoding($str, 'UTF-8', 'UTF-8');
// 保底:去除控制字符(保留换行 \r\n
$str = preg_replace('/[^\P{C}\r\n]+/u', '', $str);
return $str;
}
/**
* 更新指定禁用词的原因备注。
*
* @param int $id 记录 ID
* @param Request $request 请求体reason
*/
public function update(Request $request, int $id): JsonResponse
{
$item = UsernameBlacklist::permanent()->findOrFail($id);
$validated = $request->validate([
'reason' => ['nullable', 'string', 'max:100'],
]);
$item->update(['reason' => $validated['reason']]);
return response()->json(['status' => 'success', 'message' => '备注已更新。']);
}
/**
* 删除指定永久禁用词。
*
* @param int $id 记录 ID
*/
public function destroy(int $id): JsonResponse
{
$item = UsernameBlacklist::permanent()->findOrFail($id);
$name = $item->username;
$item->delete();
return response()->json(['status' => 'success', 'message' => "{$name}」已从永久禁用列表移除。"]);
}
}