CharController.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use App\Models\Char\CharBase;
  5. use App\Models\Char\CharCashItem_OutputBox;
  6. use App\Models\Char\CharItem;
  7. use App\Models\Char\CharFellow;
  8. use App\Models\Char\CharQuest;
  9. use App\Models\Char\CharMail;
  10. use Illuminate\Support\Facades\Log;
  11. use App\Models\Char\CharQuest_History;
  12. class CharController extends Controller
  13. {
  14. /**
  15. * Получение всех персонажей из базы данных с пагинацией
  16. * (по умолчанию - только активные персонажи, если в запросе with_deleted=true - в том числе удаленные).
  17. *
  18. * @return \Illuminate\Http\JsonResponse
  19. */
  20. public function GetAllCharacters(Request $request)
  21. {
  22. // 1. Валидация и параметры запроса
  23. $validated = $request->validate([
  24. 'per_page' => 'sometimes|integer|min:1|max:100',
  25. 'search' => 'sometimes|string|nullable|max:50',
  26. 'with_deleted' => 'sometimes'
  27. ]);
  28. // Флаг: включать ли удаленных персонажей
  29. // Используем $request->boolean(), который корректно приводит строки ('true', '1', 'on', 'yes') к true
  30. $withDeleted = $request->boolean('with_deleted');
  31. $perPage = $validated['per_page'] ?? 15;
  32. $search = trim($validated['search'] ?? '');
  33. $sortField = $request->input('sort_field', 'DBKey');
  34. $sortOrder = strtolower($request->input('sort_order', 'asc')) === 'desc' ? 'desc' : 'asc';
  35. // whitelist для полей сортировки и выборки
  36. $allowedSortFields = ['DBKey', 'Name', 'Level', 'AccountName', 'Deleted'];
  37. if (!in_array($sortField, $allowedSortFields, true)) {
  38. $sortField = 'DBKey';
  39. }
  40. // 2. Запрос с выборкой только нужных полей
  41. $query = CharBase::query()->select($allowedSortFields);
  42. // Если не запрошено with_deleted=true — возвращаем только активных персонажей (Deleted = 0)
  43. if (!$withDeleted) {
  44. $query->where('Deleted', 0);
  45. }
  46. if ($search !== '') {
  47. $query->where(function ($q) use ($search) {
  48. $q->where('Name', 'like', "%{$search}%")
  49. ->orWhere('AccountName', 'like', "%{$search}%");
  50. if (ctype_digit($search)) {
  51. $q->orWhere('DBKey', (int) $search);
  52. }
  53. });
  54. }
  55. // 3. Сортировка и пагинация
  56. $charactersPaginator = $query->orderBy($sortField, $sortOrder)->paginate($perPage);
  57. // 4. Ответ
  58. // Если по запросу не найдено ни одного персонажа, возвращаем специальный ответ
  59. // для сохранения обратной совместимости API.
  60. if ($charactersPaginator->total() === 0) {
  61. return response()->json([
  62. 'characters' => (object) [],
  63. 'code' => -2,
  64. 'msg' => 'Characters not found.'
  65. ], 200);
  66. }
  67. // Laravel автоматически преобразует пагинатор в корректный JSON,
  68. // включая только выбранные через select() поля.
  69. return response()->json([
  70. 'code' => 0,
  71. 'msg' => 'Characters successfully received.',
  72. 'characters' => $charactersPaginator,
  73. ], 200);
  74. }
  75. /**
  76. * Получение всех персонажей пользователя по имени пользователя. *
  77. * (по умолчанию - только активные персонажи, если в запросе with_deleted=true - в том числе удаленные персонажи)
  78. *
  79. * @param string $username
  80. * @return \Illuminate\Http\JsonResponse
  81. */
  82. public function GetUserCharacters($username)
  83. {
  84. // Флаг: включать ли удаленных персонажей (with_deleted=true/1/on/yes)
  85. $withDeleted = request()->boolean('with_deleted');
  86. // Получаем всех персонажей по $username
  87. $characters = CharBase::where('AccountName', $username)
  88. ->when(!$withDeleted, function ($q) {
  89. // Если не запрошены удаленные персонажи — фильтруем только активных
  90. $q->where('Deleted', 0);
  91. })
  92. ->get();
  93. // Проверяем, найдены ли персонажи
  94. if ($characters->isEmpty()) {
  95. return response()->json(['characters' => [], 'code' => -2, 'msg' => 'Characters not found for this user.'], 200);
  96. }
  97. // Подсчитываем общее время в игре для всех персонажей пользователя
  98. $totalPlayTime = $characters->sum('TotalPlayTime');
  99. // Возвращаем только DBKey, Name, Level, GuildDBKey, TotalPlayTime, Deleted для всех персонажей
  100. $characters = $characters->map(function ($character) {
  101. return [
  102. 'DBKey' => $character->DBKey,
  103. 'Name' => $character->Name,
  104. 'Level' => $character->Level,
  105. 'GuildDBKey' => $character->GuildDBKey,
  106. 'CharPlayTime' => $character->TotalPlayTime,
  107. 'Deleted' => $character->Deleted,
  108. ];
  109. });
  110. return response()->json([
  111. 'code' => 0,
  112. 'msg' => 'Characters successfully received.',
  113. 'characters' => $characters,
  114. 'totalPlayTime' => $totalPlayTime
  115. ], 200);
  116. }
  117. /**
  118. * Отправка предмета на персонажа.
  119. *
  120. * @param \Illuminate\Http\Request $request
  121. * @return \Illuminate\Http\JsonResponse
  122. */
  123. public function SendItemToCharacter(Request $request)
  124. {
  125. $character = CharBase::where('DBKey', $request->Owner)->first();
  126. if (!$character) {
  127. return response()->json(['code' => -1, 'msg' => 'Character not found.'], 404);
  128. }
  129. $validator = \Validator::make($request->all(), [
  130. 'Owner' => 'required',
  131. 'Kind' => 'required',
  132. 'RecId' => 'required',
  133. 'Amount' => 'required|numeric',
  134. 'Period' => 'required|numeric',
  135. 'evPType' => 'required',
  136. 'Comment' => 'required'
  137. ], [], [
  138. 'Owner' => '',
  139. 'Kind' => '',
  140. 'RecId' => '',
  141. 'Amount' => '',
  142. 'Period' => '',
  143. 'evPType' => '',
  144. 'Comment' => ''
  145. ]);
  146. if (!$validator->passes())
  147. return response()->json(['code' => -1, 'msg' => $validator->errors()], 400);
  148. CharCashItem_OutputBox::SENDITEMOUTPUTBOX($request);
  149. return response()->json([
  150. 'code' => 0,
  151. 'msg' => 'Items successfully sent.'
  152. ], 200);
  153. }
  154. /**
  155. * Получение всех данных персонажа из указанной таблицы.
  156. *
  157. * @param int $char_id
  158. * @param string $table_name
  159. * @return \Illuminate\Http\JsonResponse
  160. */
  161. public function GetCharacterData($char_id, $table_name)
  162. {
  163. // Получаем конфигурацию таблицы через приватный метод
  164. $config = $this->resolveTable($table_name);
  165. if (!$config) {
  166. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  167. }
  168. $modelClass = $config['model'];
  169. $key = $config['key'];
  170. // Проверяем существование персонажа
  171. if (!CharBase::find($char_id)) {
  172. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  173. }
  174. // Получаем данные
  175. $data = $modelClass::where($key, $char_id)->get();
  176. if ($data->isEmpty()) {
  177. return response()->json(['code' => -3, 'msg' => 'No data found for this character in the specified table.'], 200);
  178. }
  179. return response()->json([
  180. 'code' => 0,
  181. 'msg' => 'Data successfully received.',
  182. 'data' => $data
  183. ], 200);
  184. }
  185. /**
  186. * Обновление данных персонажа в указанной таблице.
  187. *
  188. * @param Request $request
  189. * @param int $char_id
  190. * @param string $table_name
  191. * @return \Illuminate\Http\JsonResponse
  192. */
  193. public function UpdateCharacterData(Request $request, $char_id, $table_name)
  194. {
  195. // 1. Получаем конфигурацию таблицы через приватный метод
  196. $config = $this->resolveTable($table_name);
  197. if (!$config) {
  198. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  199. }
  200. $modelClass = $config['model'];
  201. $ownerKey = $config['key']; // Ключ, по которому связывается с CharBase - id персонажа
  202. $primaryKey = $config['pk']; // Для CharBase - DBKey
  203. // 2. Определяем, какую запись обновляем
  204. if ($table_name === 'CharBase') {
  205. $model = $modelClass::find($char_id);
  206. } else {
  207. $recordId = $request->input($primaryKey);
  208. if (!$recordId) {
  209. return response()->json(['code' => -4, 'msg' => "Primary key '$primaryKey' is required for this table."], 400);
  210. }
  211. $model = $modelClass::find($recordId);
  212. }
  213. if (!$model) {
  214. return response()->json(['code' => -2, 'msg' => 'Record not found.'], 404);
  215. }
  216. // Проверяем принадлежность записи персонажу
  217. if ($model->{$ownerKey} != $char_id) {
  218. return response()->json(['code' => -5, 'msg' => 'Record does not belong to the character.'], 403);
  219. }
  220. // 3. Подготавливаем данные к обновлению – защищаем PK и владение
  221. $data = $request->except([$primaryKey, $ownerKey, 'DBKey', 'Owner', 'Account']);
  222. // 4. Обновляем модель и возвращаем ответ
  223. return $this->_updateAndRespond($model, $data);
  224. }
  225. /**
  226. * Получает конкретный предмет по $char_item_id из указанной таблицы $table предметов.
  227. *
  228. * @param string $table
  229. * @param int $char_item_id
  230. * @return \Illuminate\Http\JsonResponse
  231. */
  232. public function GetCharItem(string $table, int $char_item_id)
  233. {
  234. // Получаем конфигурацию таблицы (модель и первичный ключ)
  235. $config = $this->resolveTable($table);
  236. if (!$config) {
  237. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  238. }
  239. $modelClass = $config['model'];
  240. $pk = $config['pk'];
  241. // Ищем предмет по первичному ключу из конфигурации
  242. $item = $modelClass::where($pk, $char_item_id)->first();
  243. if (!$item) {
  244. return response()->json([
  245. 'code' => -2,
  246. 'msg' => 'Item not found.',
  247. ], 404);
  248. }
  249. return response()->json([
  250. 'code' => 0,
  251. 'msg' => 'Item successfully received.',
  252. 'data' => $item,
  253. ], 200);
  254. }
  255. /**
  256. * Обновляет конкретный предмет по $char_item_id в указанной таблице $table предметов.
  257. *
  258. * @param \Illuminate\Http\Request $request
  259. * @param string $table
  260. * @param int $char_item_id
  261. * @return \Illuminate\Http\JsonResponse
  262. */
  263. public function UpdateCharItem(Request $request, string $table, int $char_item_id)
  264. {
  265. // Получаем конфигурацию таблицы (модель и первичный ключ)
  266. $config = $this->resolveTable($table);
  267. if (!$config) {
  268. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  269. }
  270. $modelClass = $config['model'];
  271. $pk = $config['pk'];
  272. // Ищем предмет по первичному ключу из конфигурации
  273. $item = $modelClass::where($pk, $char_item_id)->first();
  274. if (!$item) {
  275. return response()->json([
  276. 'code' => -2,
  277. 'msg' => 'Item not found.',
  278. ], 404);
  279. }
  280. // Определяем защищённые поля динамически, чтобы не дать изменить PK и владельца
  281. $protected = [$pk, 'Owner', 'Account'];
  282. $data = $request->except($protected);
  283. return $this->_updateAndRespond($item, $data, 'Item successfully updated.');
  284. }
  285. /**
  286. * Получение квеста персонажа по char_id и RecId.
  287. * Поддерживается выбор таблицы (CharQuest или CharQuest_History) через опциональный параметр $table.
  288. *
  289. * @param string $char_id
  290. * @param string $rec_id
  291. * @param string|null $table
  292. * @return \Illuminate\Http\JsonResponse
  293. */
  294. public function GetCharQuest(string $char_id, string $rec_id, ?string $table = null)
  295. {
  296. // По умолчанию используем CharQuest
  297. $table = $table ?? 'CharQuest';
  298. // Проверяем допустимость таблицы для данной операции
  299. $allowedQuestTables = ['CharQuest', 'CharQuest_History'];
  300. if (!in_array($table, $allowedQuestTables, true)) {
  301. return response()->json(['code' => -1, 'msg' => 'Table not allowed for this operation.'], 400);
  302. }
  303. // Проверяем существование персонажа
  304. if (!CharBase::find($char_id)) {
  305. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  306. }
  307. // Получаем конфиг модели по таблице и делаем выборку по Owner и RecId
  308. $config = $this->resolveTable($table);
  309. $modelClass = $config['model'];
  310. $quest = $modelClass::where('Owner', $char_id)
  311. ->where('RecId', $rec_id)
  312. ->first();
  313. if (!$quest) {
  314. return response()->json([
  315. 'code' => -3,
  316. 'msg' => 'Quest not found for this character.',
  317. ], 404);
  318. }
  319. return response()->json([
  320. 'code' => 0,
  321. 'msg' => 'Quest successfully received.',
  322. 'data' => $quest,
  323. ], 200);
  324. }
  325. /**
  326. * Обновление квеста персонажа по char_id и RecId.
  327. * Поддерживается выбор таблицы (CharQuest или CharQuest_History) через опциональный параметр $table.
  328. *
  329. * @param \Illuminate\Http\Request $request
  330. * @param string $char_id
  331. * @param string $rec_id
  332. * @param string|null $table
  333. * @return \Illuminate\Http\JsonResponse
  334. */
  335. public function UpdateCharQuest(Request $request, string $char_id, string $rec_id, ?string $table = null)
  336. {
  337. // По умолчанию используем CharQuest
  338. $table = $table ?? 'CharQuest';
  339. // Проверяем допустимость таблицы для данной операции
  340. $allowedQuestTables = ['CharQuest', 'CharQuest_History'];
  341. if (!in_array($table, $allowedQuestTables, true)) {
  342. return response()->json(['code' => -1, 'msg' => 'Table not allowed for this operation.'], 400);
  343. }
  344. // Проверяем существование персонажа
  345. if (!CharBase::find($char_id)) {
  346. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  347. }
  348. // Получаем конфиг модели по таблице и делаем выборку по Owner и RecId
  349. $config = $this->resolveTable($table);
  350. $modelClass = $config['model'];
  351. $quest = $modelClass::where('Owner', $char_id)
  352. ->where('RecId', $rec_id)
  353. ->first();
  354. if (!$quest) {
  355. return response()->json([
  356. 'code' => -3,
  357. 'msg' => 'Quest not found for this character.',
  358. ], 404);
  359. }
  360. // Защищаем поля Owner и RecId от изменения
  361. $protected = ['Owner', 'RecId'];
  362. $data = $request->except($protected);
  363. return $this->_updateAndRespond($quest, $data, 'Quest successfully updated.');
  364. }
  365. //------------------------------ ПРИВАТНЫЕ МЕТОДЫ -----------------------------------------
  366. /**
  367. * Возвращает конфигурацию таблицы или null, если таблица не разрешена.
  368. *
  369. * @param string $table
  370. * @return array|null
  371. */
  372. private function resolveTable(string $table): ?array
  373. {
  374. return self::TABLES[$table] ?? null;
  375. }
  376. /**
  377. * Белый список доступных для обновления таблиц.
  378. * Используется в resolveTable(), GetCharacterData(), UpdateCharacterData().
  379. */
  380. private const TABLES = [
  381. 'CharBase' => ['key' => 'DBKey', 'model' => CharBase::class, 'pk' => 'DBKey'],
  382. 'CharItem' => ['key' => 'Owner', 'model' => CharItem::class, 'pk' => 'CharItemID'],
  383. 'CharFellow' => ['key' => 'Owner', 'model' => CharFellow::class, 'pk' => 'FellowID'],
  384. 'CharCashItem_OutputBox' => ['key' => 'Owner', 'model' => CharCashItem_OutputBox::class, 'pk' => 'ItemDBIndex'],
  385. 'CharQuest' => ['key' => 'Owner', 'model' => CharQuest::class, 'pk' => 'RecId'],
  386. 'CharQuest_History' => ['key' => 'Owner', 'model' => CharQuest_History::class, 'pk' => 'RecId'],
  387. 'CharMail' => ['key' => 'RecverDBKey', 'model' => CharMail::class, 'pk' => 'MailDBKey'],
  388. ];
  389. /**
  390. * Общий метод для обновления модели и формирования ответа.
  391. *
  392. * @param \Illuminate\Database\Eloquent\Model $model
  393. * @param array $data
  394. * @param string $successMsg
  395. * @return \Illuminate\Http\JsonResponse
  396. */
  397. private function _updateAndRespond(\Illuminate\Database\Eloquent\Model $model, array $data, string $successMsg = 'Data successfully updated.')
  398. {
  399. // 1. Фильтруем поля для массового присвоения
  400. $fillable = $model->getFillable();
  401. $guarded = $model->getGuarded();
  402. if (!empty($fillable)) {
  403. // Если есть fillable - используем только их
  404. $filteredData = array_intersect_key($data, array_flip($fillable));
  405. } elseif ($guarded === ['*']) {
  406. // Если guarded = ['*'] - запрещено массовое присвоение
  407. return response()->json(['code' => -6, 'msg' => 'Mass assignment is not allowed for this model.'], 400);
  408. } else {
  409. // Если есть guarded (но не ['*']) - исключаем только guarded поля
  410. $filteredData = array_diff_key($data, array_flip($guarded));
  411. }
  412. // 2. Конвертируем null-поля в '', чтобы вместо NULL в БД сохранялась пустая строка
  413. $filteredData = array_map(static fn($v) => $v === null ? '' : $v, $filteredData);
  414. if (empty($filteredData)) {
  415. return response()->json(['code' => -6, 'msg' => 'No valid fields to update.'], 400);
  416. }
  417. // 3. Пытаемся обновить запись
  418. try {
  419. $ok = $model->update($filteredData);
  420. } catch (\Throwable $e) {
  421. Log::error('Database error while updating record:', ['error' => $e->getMessage()]);
  422. return response()->json([
  423. 'code' => -7,
  424. 'msg' => 'Database error while updating record.',
  425. 'error' => $e->getMessage(),
  426. ], 500);
  427. }
  428. if (!$ok) {
  429. return response()->json([
  430. 'code' => -8,
  431. 'msg' => 'Update failed, record not modified.',
  432. ], 500);
  433. }
  434. // 4. Возвращаем успешный ответ
  435. return response()->json([
  436. 'code' => 0,
  437. 'msg' => $successMsg,
  438. 'data' => $model->fresh()
  439. ], 200);
  440. }
  441. }