CharController.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  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\DB;
  11. use Illuminate\Support\Facades\Log;
  12. use Illuminate\Support\Facades\Validator;
  13. use App\Models\Char\CharQuest_History;
  14. class CharController extends Controller
  15. {
  16. /**
  17. * Получение всех персонажей из базы данных с пагинацией
  18. * (по умолчанию - только активные персонажи, если в запросе with_deleted=true - в том числе удаленные).
  19. *
  20. * @return \Illuminate\Http\JsonResponse
  21. */
  22. public function GetAllCharacters(Request $request)
  23. {
  24. // 1. Валидация и параметры запроса
  25. $validated = $request->validate([
  26. 'per_page' => 'sometimes|integer|min:1|max:100',
  27. 'search' => 'sometimes|string|nullable|max:50',
  28. 'with_deleted' => 'sometimes'
  29. ]);
  30. // Флаг: включать ли удаленных персонажей
  31. // Используем $request->boolean(), который корректно приводит строки ('true', '1', 'on', 'yes') к true
  32. $withDeleted = $request->boolean('with_deleted');
  33. $perPage = $validated['per_page'] ?? 15;
  34. $search = trim($validated['search'] ?? '');
  35. $sortField = $request->input('sort_field', 'DBKey');
  36. $sortOrder = strtolower($request->input('sort_order', 'asc')) === 'desc' ? 'desc' : 'asc';
  37. // whitelist для полей сортировки и выборки
  38. $allowedSortFields = ['DBKey', 'Name', 'Level', 'AccountName', 'Deleted'];
  39. if (!in_array($sortField, $allowedSortFields, true)) {
  40. $sortField = 'DBKey';
  41. }
  42. // 2. Запрос с выборкой только нужных полей
  43. $query = CharBase::query()->select($allowedSortFields);
  44. // Если не запрошено with_deleted=true — возвращаем только активных персонажей (Deleted = 0)
  45. if (!$withDeleted) {
  46. $query->where('Deleted', 0);
  47. }
  48. if ($search !== '') {
  49. $query->where(function ($q) use ($search) {
  50. $q->where('Name', 'like', "%{$search}%")
  51. ->orWhere('AccountName', 'like', "%{$search}%");
  52. if (ctype_digit($search)) {
  53. $q->orWhere('DBKey', (int) $search);
  54. }
  55. });
  56. }
  57. // 3. Сортировка и пагинация
  58. $charactersPaginator = $query->orderBy($sortField, $sortOrder)->paginate($perPage);
  59. // 4. Ответ
  60. // Если по запросу не найдено ни одного персонажа, возвращаем специальный ответ
  61. // для сохранения обратной совместимости API.
  62. if ($charactersPaginator->total() === 0) {
  63. return response()->json([
  64. 'characters' => (object) [],
  65. 'code' => -2,
  66. 'msg' => 'Characters not found.'
  67. ], 200);
  68. }
  69. // Laravel автоматически преобразует пагинатор в корректный JSON,
  70. // включая только выбранные через select() поля.
  71. return response()->json([
  72. 'code' => 0,
  73. 'msg' => 'Characters successfully received.',
  74. 'characters' => $charactersPaginator,
  75. ], 200);
  76. }
  77. /**
  78. * Получение всех персонажей пользователя по имени пользователя. *
  79. * (по умолчанию - только активные персонажи, если в запросе with_deleted=true - в том числе удаленные персонажи)
  80. *
  81. * @param string $username
  82. * @return \Illuminate\Http\JsonResponse
  83. */
  84. public function GetUserCharacters($username)
  85. {
  86. // Флаг: включать ли удаленных персонажей (with_deleted=true/1/on/yes)
  87. $withDeleted = request()->boolean('with_deleted');
  88. // Получаем всех персонажей по $username
  89. $characters = CharBase::where('AccountName', $username)
  90. ->when(!$withDeleted, function ($q) {
  91. // Если не запрошены удаленные персонажи — фильтруем только активных
  92. $q->where('Deleted', 0);
  93. })
  94. ->get();
  95. // Проверяем, найдены ли персонажи
  96. if ($characters->isEmpty()) {
  97. return response()->json(['characters' => [], 'code' => -2, 'msg' => 'Characters not found for this user.'], 200);
  98. }
  99. // Подсчитываем общее время в игре для всех персонажей пользователя
  100. $totalPlayTime = $characters->sum('TotalPlayTime');
  101. // Возвращаем только DBKey, Name, Level, GuildDBKey, TotalPlayTime, Deleted для всех персонажей
  102. $characters = $characters->map(function ($character) {
  103. return [
  104. 'DBKey' => $character->DBKey,
  105. 'Name' => $character->Name,
  106. 'Level' => $character->Level,
  107. 'GuildDBKey' => $character->GuildDBKey,
  108. 'CharPlayTime' => $character->TotalPlayTime,
  109. 'Deleted' => $character->Deleted,
  110. ];
  111. });
  112. return response()->json([
  113. 'code' => 0,
  114. 'msg' => 'Characters successfully received.',
  115. 'characters' => $characters,
  116. 'totalPlayTime' => $totalPlayTime
  117. ], 200);
  118. }
  119. /**
  120. * Отправка предмета на персонажа.
  121. *
  122. * @param \Illuminate\Http\Request $request
  123. * @return \Illuminate\Http\JsonResponse
  124. */
  125. public function SendItemToCharacter(Request $request)
  126. {
  127. $character = CharBase::where('DBKey', $request->Owner)->first();
  128. if (!$character) {
  129. return response()->json(['code' => -1, 'msg' => 'Character not found.'], 404);
  130. }
  131. $validator = \Validator::make($request->all(), [
  132. 'Owner' => 'required',
  133. 'Kind' => 'required',
  134. 'RecId' => 'required',
  135. 'Amount' => 'required|numeric',
  136. 'Period' => 'required|numeric',
  137. 'evPType' => 'required',
  138. 'Comment' => 'required'
  139. ], [], [
  140. 'Owner' => '',
  141. 'Kind' => '',
  142. 'RecId' => '',
  143. 'Amount' => '',
  144. 'Period' => '',
  145. 'evPType' => '',
  146. 'Comment' => ''
  147. ]);
  148. if (!$validator->passes())
  149. return response()->json(['code' => -1, 'msg' => $validator->errors()], 400);
  150. CharCashItem_OutputBox::SENDITEMOUTPUTBOX($request);
  151. return response()->json([
  152. 'code' => 0,
  153. 'msg' => 'Items successfully sent.'
  154. ], 200);
  155. }
  156. /**
  157. * Получение всех данных персонажа из указанной таблицы.
  158. *
  159. * @param int $char_id
  160. * @param string $table_name
  161. * @return \Illuminate\Http\JsonResponse
  162. */
  163. public function GetCharacterData($char_id, $table_name)
  164. {
  165. // Получаем конфигурацию таблицы через приватный метод
  166. $config = $this->resolveTable($table_name);
  167. if (!$config) {
  168. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  169. }
  170. $modelClass = $config['model'];
  171. $key = $config['key'];
  172. // Проверяем существование персонажа
  173. if (!CharBase::find($char_id)) {
  174. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  175. }
  176. // Получаем данные
  177. $data = $modelClass::where($key, $char_id)->get();
  178. if ($data->isEmpty()) {
  179. return response()->json(['code' => -3, 'msg' => 'No data found for this character in the specified table.'], 200);
  180. }
  181. return response()->json([
  182. 'code' => 0,
  183. 'msg' => 'Data successfully received.',
  184. 'data' => $data
  185. ], 200);
  186. }
  187. /**
  188. * Обновление данных персонажа в указанной таблице.
  189. *
  190. * @param Request $request
  191. * @param int $char_id
  192. * @param string $table_name
  193. * @return \Illuminate\Http\JsonResponse
  194. */
  195. public function UpdateCharacterData(Request $request, $char_id, $table_name)
  196. {
  197. // 1. Получаем конфигурацию таблицы через приватный метод
  198. $config = $this->resolveTable($table_name);
  199. if (!$config) {
  200. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  201. }
  202. $modelClass = $config['model'];
  203. $ownerKey = $config['key']; // Ключ, по которому связывается с CharBase - id персонажа
  204. $primaryKey = $config['pk']; // Для CharBase - DBKey
  205. // 2. Определяем, какую запись обновляем
  206. if ($table_name === 'CharBase') {
  207. $model = $modelClass::find($char_id);
  208. } else {
  209. $recordId = $request->input($primaryKey);
  210. if (!$recordId) {
  211. return response()->json(['code' => -4, 'msg' => "Primary key '$primaryKey' is required for this table."], 400);
  212. }
  213. $model = $modelClass::find($recordId);
  214. }
  215. if (!$model) {
  216. return response()->json(['code' => -2, 'msg' => 'Record not found.'], 404);
  217. }
  218. // Проверяем принадлежность записи персонажу
  219. if ($model->{$ownerKey} != $char_id) {
  220. return response()->json(['code' => -5, 'msg' => 'Record does not belong to the character.'], 403);
  221. }
  222. // 3. Подготавливаем данные к обновлению – защищаем PK и владение
  223. $data = $request->except([$primaryKey, $ownerKey, 'DBKey', 'Owner', 'Account']);
  224. // 4. Обновляем модель и возвращаем ответ
  225. return $this->_updateAndRespond($model, $data);
  226. }
  227. /**
  228. * Получает конкретный предмет по $char_item_id из указанной таблицы $table предметов.
  229. *
  230. * @param string $table
  231. * @param int $char_item_id
  232. * @return \Illuminate\Http\JsonResponse
  233. */
  234. public function GetCharItem(string $table, int $char_item_id)
  235. {
  236. // Получаем конфигурацию таблицы (модель и первичный ключ)
  237. $config = $this->resolveTable($table);
  238. if (!$config) {
  239. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  240. }
  241. $modelClass = $config['model'];
  242. $pk = $config['pk'];
  243. // Ищем предмет по первичному ключу из конфигурации
  244. $item = $modelClass::where($pk, $char_item_id)->first();
  245. if (!$item) {
  246. return response()->json([
  247. 'code' => -2,
  248. 'msg' => 'Item not found.',
  249. ], 404);
  250. }
  251. return response()->json([
  252. 'code' => 0,
  253. 'msg' => 'Item successfully received.',
  254. 'data' => $item,
  255. ], 200);
  256. }
  257. /**
  258. * Обновляет конкретный предмет по $char_item_id в указанной таблице $table предметов.
  259. *
  260. * @param \Illuminate\Http\Request $request
  261. * @param string $table
  262. * @param int $char_item_id
  263. * @return \Illuminate\Http\JsonResponse
  264. */
  265. public function UpdateCharItem(Request $request, string $table, int $char_item_id)
  266. {
  267. // Получаем конфигурацию таблицы (модель и первичный ключ)
  268. $config = $this->resolveTable($table);
  269. if (!$config) {
  270. return response()->json(['code' => -1, 'msg' => 'Table not allowed or does not exist.'], 400);
  271. }
  272. $modelClass = $config['model'];
  273. $pk = $config['pk'];
  274. // Ищем предмет по первичному ключу из конфигурации
  275. $item = $modelClass::where($pk, $char_item_id)->first();
  276. if (!$item) {
  277. return response()->json([
  278. 'code' => -2,
  279. 'msg' => 'Item not found.',
  280. ], 404);
  281. }
  282. // Определяем защищённые поля динамически, чтобы не дать изменить PK и владельца
  283. $protected = [$pk, 'Owner', 'Account'];
  284. $data = $request->except($protected);
  285. return $this->_updateAndRespond($item, $data, 'Item successfully updated.');
  286. }
  287. /**
  288. * Получение квеста персонажа по char_id и RecId.
  289. * Поддерживается выбор таблицы (CharQuest или CharQuest_History) через опциональный параметр $table.
  290. *
  291. * @param string $char_id
  292. * @param string $rec_id
  293. * @param string|null $table
  294. * @return \Illuminate\Http\JsonResponse
  295. */
  296. public function GetCharQuest(string $char_id, string $rec_id, ?string $table = null)
  297. {
  298. // По умолчанию используем CharQuest
  299. $table = $table ?? 'CharQuest';
  300. // Проверяем допустимость таблицы для данной операции
  301. $allowedQuestTables = ['CharQuest', 'CharQuest_History'];
  302. if (!in_array($table, $allowedQuestTables, true)) {
  303. return response()->json(['code' => -1, 'msg' => 'Table not allowed for this operation.'], 400);
  304. }
  305. // Проверяем существование персонажа
  306. if (!CharBase::find($char_id)) {
  307. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  308. }
  309. // Получаем конфиг модели по таблице и делаем выборку по Owner и RecId
  310. $config = $this->resolveTable($table);
  311. $modelClass = $config['model'];
  312. $quest = $modelClass::where('Owner', $char_id)
  313. ->where('RecId', $rec_id)
  314. ->first();
  315. if (!$quest) {
  316. return response()->json([
  317. 'code' => -3,
  318. 'msg' => 'Quest not found for this character.',
  319. ], 404);
  320. }
  321. return response()->json([
  322. 'code' => 0,
  323. 'msg' => 'Quest successfully received.',
  324. 'data' => $quest,
  325. ], 200);
  326. }
  327. /**
  328. * Обновление квеста персонажа по char_id и RecId.
  329. * Поддерживается выбор таблицы (CharQuest или CharQuest_History) через опциональный параметр $table.
  330. *
  331. * @param \Illuminate\Http\Request $request
  332. * @param string $char_id
  333. * @param string $rec_id
  334. * @param string|null $table
  335. * @return \Illuminate\Http\JsonResponse
  336. */
  337. public function UpdateCharQuest(Request $request, string $char_id, string $rec_id, ?string $table = null)
  338. {
  339. // По умолчанию используем CharQuest
  340. $table = $table ?? 'CharQuest';
  341. // Проверяем допустимость таблицы для данной операции
  342. $allowedQuestTables = ['CharQuest', 'CharQuest_History'];
  343. if (!in_array($table, $allowedQuestTables, true)) {
  344. return response()->json(['code' => -1, 'msg' => 'Table not allowed for this operation.'], 400);
  345. }
  346. // Проверяем существование персонажа
  347. if (!CharBase::find($char_id)) {
  348. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  349. }
  350. // Получаем конфиг модели по таблице и делаем выборку по Owner и RecId
  351. $config = $this->resolveTable($table);
  352. $modelClass = $config['model'];
  353. $quest = $modelClass::where('Owner', $char_id)
  354. ->where('RecId', $rec_id)
  355. ->first();
  356. if (!$quest) {
  357. return response()->json([
  358. 'code' => -3,
  359. 'msg' => 'Quest not found for this character.',
  360. ], 404);
  361. }
  362. // Защищаем поля Owner и RecId от изменения
  363. $protected = ['Owner', 'RecId'];
  364. $data = $request->except($protected);
  365. return $this->_updateAndRespond($quest, $data, 'Quest successfully updated.');
  366. }
  367. /**
  368. * Получение письма персонажа по char_id и MailDBKey.
  369. * Возвращает ту же структуру записи, что и GetCharacterData/CharMail.
  370. *
  371. * @param string $char_id
  372. * @param string $mail_id
  373. * @return \Illuminate\Http\JsonResponse
  374. */
  375. public function GetCharMail(string $char_id, string $mail_id)
  376. {
  377. // Проверяем существование персонажа
  378. if (!CharBase::find($char_id)) {
  379. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  380. }
  381. // Получаем письмо, принадлежащее персонажу
  382. $mail = CharMail::where('RecverDBKey', $char_id)
  383. ->where('MailDBKey', $mail_id)
  384. ->first();
  385. if (!$mail) {
  386. return response()->json([
  387. 'code' => -3,
  388. 'msg' => 'Mail not found for this character.',
  389. ], 404);
  390. }
  391. return response()->json([
  392. 'code' => 0,
  393. 'msg' => 'Mail successfully received.',
  394. 'data' => $mail,
  395. ], 200);
  396. }
  397. /**
  398. * Обновление письма персонажа по char_id и MailDBKey.
  399. * Принимает те же поля, что и UpdateCharacterData/CharMail, защищая ключи владения и PK.
  400. *
  401. * @param \Illuminate\Http\Request $request
  402. * @param string $char_id
  403. * @param string $mail_id
  404. * @return \Illuminate\Http\JsonResponse
  405. */
  406. public function UpdateCharMail(Request $request, string $char_id, string $mail_id)
  407. {
  408. // Проверяем существование персонажа
  409. if (!CharBase::find($char_id)) {
  410. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  411. }
  412. // Получаем письмо, принадлежащее персонажу
  413. $mail = CharMail::where('RecverDBKey', $char_id)
  414. ->where('MailDBKey', $mail_id)
  415. ->first();
  416. if (!$mail) {
  417. return response()->json([
  418. 'code' => -3,
  419. 'msg' => 'Mail not found for this character.',
  420. ], 404);
  421. }
  422. // Защищаем поля связи и первичный ключ
  423. $protected = ['RecverDBKey', 'MailDBKey', 'Owner', 'Account'];
  424. $data = $request->except($protected);
  425. return $this->_updateAndRespond($mail, $data, 'Mail successfully updated.');
  426. }
  427. //------------------------------ ПРИВАТНЫЕ МЕТОДЫ -----------------------------------------
  428. /**
  429. * Возвращает конфигурацию таблицы или null, если таблица не разрешена.
  430. *
  431. * @param string $table
  432. * @return array|null
  433. */
  434. private function resolveTable(string $table): ?array
  435. {
  436. return self::TABLES[$table] ?? null;
  437. }
  438. /**
  439. * Белый список доступных для обновления таблиц.
  440. * Используется в resolveTable(), GetCharacterData(), UpdateCharacterData().
  441. */
  442. private const TABLES = [
  443. 'CharBase' => ['key' => 'DBKey', 'model' => CharBase::class, 'pk' => 'DBKey'],
  444. 'CharItem' => ['key' => 'Owner', 'model' => CharItem::class, 'pk' => 'CharItemID'],
  445. 'CharFellow' => ['key' => 'Owner', 'model' => CharFellow::class, 'pk' => 'FellowID'],
  446. 'CharCashItem_OutputBox' => ['key' => 'Owner', 'model' => CharCashItem_OutputBox::class, 'pk' => 'ItemDBIndex'],
  447. 'CharQuest' => ['key' => 'Owner', 'model' => CharQuest::class, 'pk' => 'RecId'],
  448. 'CharQuest_History' => ['key' => 'Owner', 'model' => CharQuest_History::class, 'pk' => 'RecId'],
  449. 'CharMail' => ['key' => 'RecverDBKey', 'model' => CharMail::class, 'pk' => 'MailDBKey'],
  450. ];
  451. /**
  452. * Общий метод для обновления модели и формирования ответа.
  453. *
  454. * @param \Illuminate\Database\Eloquent\Model $model
  455. * @param array $data
  456. * @param string $successMsg
  457. * @return \Illuminate\Http\JsonResponse
  458. */
  459. private function _updateAndRespond(\Illuminate\Database\Eloquent\Model $model, array $data, string $successMsg = 'Data successfully updated.')
  460. {
  461. // 1. Фильтруем поля для массового присвоения
  462. $fillable = $model->getFillable();
  463. $guarded = $model->getGuarded();
  464. if (!empty($fillable)) {
  465. // Если есть fillable - используем только их
  466. $filteredData = array_intersect_key($data, array_flip($fillable));
  467. } elseif ($guarded === ['*']) {
  468. // Если guarded = ['*'] - запрещено массовое присвоение
  469. return response()->json(['code' => -6, 'msg' => 'Mass assignment is not allowed for this model.'], 400);
  470. } else {
  471. // Если есть guarded (но не ['*']) - исключаем только guarded поля
  472. $filteredData = array_diff_key($data, array_flip($guarded));
  473. }
  474. // 2. Конвертируем null-поля в '', чтобы вместо NULL в БД сохранялась пустая строка
  475. $filteredData = array_map(static fn($v) => $v === null ? '' : $v, $filteredData);
  476. if (empty($filteredData)) {
  477. return response()->json(['code' => -6, 'msg' => 'No valid fields to update.'], 400);
  478. }
  479. // 3. Пытаемся обновить запись
  480. try {
  481. $ok = $model->update($filteredData);
  482. } catch (\Throwable $e) {
  483. Log::error('Database error while updating record:', ['error' => $e->getMessage()]);
  484. return response()->json([
  485. 'code' => -7,
  486. 'msg' => 'Database error while updating record.',
  487. 'error' => $e->getMessage(),
  488. ], 500);
  489. }
  490. if (!$ok) {
  491. return response()->json([
  492. 'code' => -8,
  493. 'msg' => 'Update failed, record not modified.',
  494. ], 500);
  495. }
  496. // 4. Возвращаем успешный ответ
  497. return response()->json([
  498. 'code' => 0,
  499. 'msg' => $successMsg,
  500. 'data' => $model->fresh()
  501. ], 200);
  502. }
  503. public function SendItemViaMail(Request $request)
  504. {
  505. $validator = Validator::make($request->all(), [
  506. 'ReceiverID' => 'required|integer|exists:Char.dbo.Table_CharBase,DBKey',
  507. 'StorageID' => 'sometimes|integer|exists:Char.dbo.Table_CharBase,DBKey',
  508. 'RecId' => 'required|string|max:20',
  509. 'Amount' => 'required|integer|min:1|max:9999',
  510. 'Title' => 'sometimes|string|nullable|max:33',
  511. 'Letter' => 'sometimes|string|nullable|max:512',
  512. 'Money' => 'sometimes|integer|min:0',
  513. ]);
  514. if ($validator->fails()) {
  515. return response()->json(['code' => -1, 'msg' => $validator->errors()], 400);
  516. }
  517. try {
  518. return DB::connection('Char')->transaction(function () use ($request) {
  519. $receiverId = $request->input('ReceiverID');
  520. $storageId = $request->input('StorageID', $receiverId);
  521. $receiver = CharBase::where('DBKey', $receiverId)->lockForUpdate()->first();
  522. if (!$receiver) {
  523. return response()->json(['code' => -2, 'msg' => 'Character not found.'], 404);
  524. }
  525. $recycledMail = CharMail::where('RecverDBKey', $receiver->DBKey)
  526. ->where('Deleted', 1)
  527. ->orderBy('Slot')
  528. ->lockForUpdate()
  529. ->first();
  530. $isRecycled = false;
  531. if ($recycledMail) {
  532. $nextMailSlot = $recycledMail->Slot;
  533. $mailDBKey = $recycledMail->MailDBKey;
  534. $isRecycled = true;
  535. } else {
  536. $maxMailSlot = CharMail::where('RecverDBKey', $receiver->DBKey)
  537. ->lockForUpdate()
  538. ->max('Slot');
  539. $nextMailSlot = ($maxMailSlot !== null) ? $maxMailSlot + 1 : 0;
  540. $mailDBKey = null;
  541. }
  542. $itemCount = 1;
  543. $itemSlots = [-1, -1, -1, -1, -1];
  544. $itemsData = [];
  545. $maxMailItemSlot = CharItem::where('Owner', $receiver->DBKey)
  546. ->where('StrRecordKind', 'ml')
  547. ->lockForUpdate()
  548. ->max('Slot');
  549. $nextItemSlot = ($maxMailItemSlot !== null) ? $maxMailItemSlot + 1 : 0;
  550. $storage = CharBase::where('DBKey', $storageId)->lockForUpdate()->first();
  551. if (!$storage) {
  552. return response()->json(['code' => -2, 'msg' => 'Storage character not found.'], 404);
  553. }
  554. for ($i = 0; $i < $itemCount; $i++) {
  555. $storage->ItemSerialOrder += 1;
  556. $itemSerial = ($storage->ItemSerialOrder * 4294967296) + $storage->DBKey;
  557. $itemSlots[$i] = $nextItemSlot;
  558. $itemsData[] = array_merge([
  559. "Owner" => $receiver->DBKey,
  560. "Slot" => $nextItemSlot,
  561. "ItemSerial" => $itemSerial,
  562. "KeyValueA" => $itemSerial,
  563. "StrRecordKind" => "ml",
  564. "Account" => 0,
  565. "RecId" => $request->input('RecId'),
  566. "Amount" => $request->input('Amount'),
  567. "ClientSlot" => -1,
  568. "Storage" => "in",
  569. "ItemGrade" => "normal",
  570. "Durability" => 100,
  571. "MailDbKey" => null,
  572. "Deleted" => 0,
  573. ], $request->input('item_overrides', []));
  574. $nextItemSlot++;
  575. }
  576. $storage->save();
  577. $titleText = $request->input('Title', 'Системное сообщение');
  578. $letterText = $request->input('Letter', 'Вам прислан подарок.');
  579. $money = $request->input('Money', 0);
  580. if ($isRecycled) {
  581. $recycledMail->fill([
  582. 'Deleted' => 0,
  583. 'bRead' => 0,
  584. 'MailType' => 'es',
  585. 'SentMailDBKey' => 0,
  586. 'SenderDBKeyAccount' => -2,
  587. 'SenderDBKey' => -2,
  588. 'SendName' => 'GM',
  589. 'RecvName' => $receiver->Name,
  590. 'Title' => $titleText,
  591. 'Letter' => $letterText,
  592. 'binTitle' => $titleText,
  593. 'binLetter' => $letterText,
  594. 'CreateDate' => now(),
  595. 'ReadDate' => now(),
  596. 'Money' => $money,
  597. 'AlreadyMoney' => 0,
  598. 'ItemCount' => $itemCount,
  599. 'ItemSlot0' => -1,
  600. 'ItemSlot1' => -1,
  601. 'ItemSlot2' => -1,
  602. 'ItemSlot3' => -1,
  603. 'ItemSlot4' => -1,
  604. 'AlreadyItem0' => 0,
  605. 'AlreadyItem1' => 0,
  606. 'AlreadyItem2' => 0,
  607. 'AlreadyItem3' => 0,
  608. 'AlreadyItem4' => 0,
  609. 'Confirm' => 1,
  610. ]);
  611. $recycledMail->save();
  612. CharItem::where('Owner', $receiver->DBKey)
  613. ->where('MailDbKey', $mailDBKey)
  614. ->where('StrRecordKind', 'ml')
  615. ->update([
  616. 'Deleted' => 1,
  617. 'RecId' => '*',
  618. 'ItemSerial' => 0,
  619. ]);
  620. } else {
  621. $mailData = [
  622. "RecverDBKey" => $receiver->DBKey,
  623. "RecvName" => $receiver->Name,
  624. "Slot" => $nextMailSlot,
  625. "MailType" => "es",
  626. "SentMailDBKey" => 0,
  627. "SenderDBKeyAccount" => -2,
  628. "SenderDBKey" => -2,
  629. "SendName" => "GM",
  630. "ItemCount" => $itemCount,
  631. "ItemSlot0" => -1,
  632. "ItemSlot1" => -1,
  633. "ItemSlot2" => -1,
  634. "ItemSlot3" => -1,
  635. "ItemSlot4" => -1,
  636. "AlreadyItem0" => 0,
  637. "AlreadyItem1" => 0,
  638. "AlreadyItem2" => 0,
  639. "AlreadyItem3" => 0,
  640. "AlreadyItem4" => 0,
  641. "Confirm" => 1,
  642. "Title" => $titleText,
  643. "Letter" => $letterText,
  644. "binTitle" => $titleText,
  645. "binLetter" => $letterText,
  646. "Money" => $money,
  647. "AlreadyMoney" => 0,
  648. "CreateDate" => now(),
  649. "ReadDate" => now(),
  650. "Deleted" => 0,
  651. ];
  652. $mail = CharMail::create($mailData);
  653. $mailDBKey = $mail->MailDBKey;
  654. }
  655. foreach ($itemsData as $itemData) {
  656. $itemData['MailDbKey'] = $mailDBKey;
  657. CharItem::create($itemData);
  658. }
  659. if ($itemCount >= 1) {
  660. $mailUpdateData = [];
  661. for ($i = 0; $i < 5; $i++) {
  662. $mailUpdateData["ItemSlot{$i}"] = $itemSlots[$i] ?? -1;
  663. }
  664. CharMail::where('MailDBKey', $mailDBKey)->update($mailUpdateData);
  665. }
  666. return response()->json([
  667. 'code' => 0,
  668. 'msg' => 'Item sent successfully.',
  669. 'data' => [
  670. 'MailDBKey' => $mailDBKey,
  671. 'ItemSerial' => (string)$itemsData[0]['ItemSerial'],
  672. 'MailSlot' => $nextMailSlot,
  673. 'ItemSlot' => $itemSlots[0],
  674. 'IsRecycled' => $isRecycled,
  675. ]
  676. ], 200);
  677. });
  678. } catch (\Throwable $e) {
  679. Log::error('SendItemViaMail Error: ' . $e->getMessage(), [
  680. 'trace' => $e->getTraceAsString(),
  681. 'ReceiverID' => $request->input('ReceiverID'),
  682. ]);
  683. return response()->json(['code' => -3, 'msg' => 'Error: ' . $e->getMessage()], 500);
  684. }
  685. }
  686. }