Banner.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. namespace App\Http\Controllers\admin;
  3. use App\Constants\Util;
  4. use App\Constants\HttpStatus;
  5. use App\Http\Controllers\Controller;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Validation\ValidationException;
  8. use Exception;
  9. use App\Models\Banner as BannerModel;
  10. use App\Services\BannerVisibilityService;
  11. use App\Support\ContentLink;
  12. class Banner extends Controller
  13. {
  14. public const CLICK_TYPES = ['none' => '无链接', 'activity' => '跳转活动', 'external' => '外站链接', 'internal' => '站内相对路径'];
  15. /**
  16. * Resolve the submitted jump target. Omitting every link field keeps the saved one; old clients that
  17. * only send `link` get the type inferred from it (activity ID, `/path` or full URL).
  18. */
  19. private static function clickBehavior(array $params, BannerModel $banner): array
  20. {
  21. $current = $banner->exists ? BannerVisibilityService::links($banner->getAttributes())
  22. : ['click_type' => 'none', 'link' => null, 'link_pc' => null, 'link_h5' => null];
  23. $input = array_intersect_key($params, array_flip(ContentLink::FIELDS));
  24. if (!$input) return $current;
  25. if (array_key_exists('link', $input) && $input['link'] !== null) $input['link'] = trim((string) $input['link']);
  26. if (!array_key_exists('click_type', $input)) {
  27. $input['click_type'] = array_key_exists('link', $input)
  28. ? BannerVisibilityService::clickType(null, $input['link']) : $current['click_type'];
  29. }
  30. if ($input['click_type'] === 'activity') {
  31. $link = array_key_exists('link', $input) ? $input['link'] : ($current['click_type'] === 'activity' ? $current['link'] : null);
  32. $id = BannerVisibilityService::activityId($link);
  33. if ($id === null) throw ValidationException::withMessages(['link' => '请填写跳转活动ID']);
  34. return ['click_type' => 'activity', 'link' => $id, 'link_pc' => null, 'link_h5' => null];
  35. }
  36. // An activity-type row has no reusable external/internal link, so forSave sees it as "none".
  37. return ContentLink::forSave($input, $current['click_type'] === 'activity' ? [] : $current);
  38. }
  39. /**
  40. * @api {get} /admin/banner/index 轮播图列表
  41. * @apiGroup 轮播图管理
  42. * @apiUse result
  43. * @apiUse header
  44. * @apiVersion 1.0.0
  45. *
  46. * @apiParam {int} [page=1]
  47. * @apiParam {int} [limit=10]
  48. *
  49. */
  50. function index()
  51. {
  52. try {
  53. $params = request()->validate([
  54. 'page' => ['sometimes', 'integer', 'min:1'],
  55. 'limit' => ['sometimes', 'integer', 'min:1', 'max:200'],
  56. 'type' => ['nullable', 'in:pc,app'],
  57. 'platform' => ['nullable', 'in:pc,h5'],
  58. 'status' => ['nullable', 'integer', 'in:0,1'],
  59. 'lang' => ['nullable', 'string', 'max:10'],
  60. ]);
  61. $page = request()->input('page', 1);
  62. $limit = request()->input('limit', 15);
  63. $type = $params['type'] ?? '';
  64. if (isset($params['platform']) && $params['platform'] !== '') {
  65. // The existing mobile banner enum is "app"; H5 filtering preserves that storage contract.
  66. $platformType = $params['platform'] === 'pc' ? 'pc' : 'app';
  67. if ($type !== '' && $type !== $platformType) {
  68. throw ValidationException::withMessages(['platform' => '平台与原类型筛选不一致']);
  69. }
  70. $type = $platformType;
  71. }
  72. $query = BannerModel::query();
  73. if ($type !== '') {
  74. $query->where('type', $type);
  75. }
  76. if (isset($params['lang']) && $params['lang'] !== '') {
  77. $query->where('lang', $params['lang']);
  78. }
  79. $query->orderByDesc('sort')->orderByDesc('id');
  80. if (isset($params['status']) && $params['status'] !== '') {
  81. // Stored manual status is not the linked activity's actual status. Filter before pagination.
  82. $rows = BannerVisibilityService::decorate($query->get());
  83. $rows = array_values(array_filter($rows, static fn ($row) => $row['status'] === (int) $params['status']));
  84. $count = count($rows);
  85. $list = array_slice($rows, ((int) $page - 1) * (int) $limit, (int) $limit);
  86. } else {
  87. $count = $query->count();
  88. $list = BannerVisibilityService::decorate($query->forPage($page, $limit)->get());
  89. }
  90. } catch (ValidationException $e) {
  91. return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
  92. } catch (Exception $e) {
  93. return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
  94. }
  95. return $this->success(['total' => $count, 'data' => $list]);
  96. }
  97. /**
  98. * @api {post} /admin/banner/update 修改
  99. * @apiDescription 更新或者新增都用这个接口
  100. * @apiGroup 轮播图管理
  101. * @apiUse result
  102. * @apiUse header
  103. * @apiVersion 1.0.0
  104. *
  105. */
  106. public function update()
  107. {
  108. DB::beginTransaction();
  109. try {
  110. $params = request()->validate([
  111. 'id' => ['nullable', 'integer', 'min:1'],
  112. 'title' => ['required'],
  113. 'image' => ['required'],
  114. 'click_type' => ['sometimes', 'string', 'in:' . implode(',', array_keys(self::CLICK_TYPES))],
  115. 'link' => ['nullable', function ($attribute, $value, $fail) {
  116. if ((!is_string($value) && !is_int($value)) || strlen((string) $value) > 2048) {
  117. $fail('跳转ID或链接格式错误');
  118. }
  119. }],
  120. 'link_pc' => ['nullable', 'string', 'max:2048'],
  121. 'link_h5' => ['nullable', 'string', 'max:2048'],
  122. 'type' => ['required', 'in:pc,app'],
  123. 'sort' => ['nullable', 'integer'],
  124. 'lang' => ['required'],
  125. 'status' => ['sometimes', 'integer', 'in:0,1'],
  126. 'manual_status' => ['nullable', 'integer', 'in:0,1'],
  127. ], ['click_type.in' => '点击方式仅支持无链接、跳转活动、外站链接、站内相对路径']);
  128. $id = $params['id'] ?? null;
  129. $banner = $id === null ? new BannerModel() : BannerModel::where('id', $id)->lockForUpdate()->first();
  130. if (!$banner) throw new Exception('数据不存在', HttpStatus::CUSTOM_ERROR);
  131. $links = self::clickBehavior($params, $banner);
  132. $activityId = BannerVisibilityService::boundActivityId($links['click_type'], $links['link']);
  133. $previousActivityId = BannerVisibilityService::boundActivityId($banner->click_type, $banner->link);
  134. if ($activityId !== null && (!$banner->exists || $activityId !== $previousActivityId)) {
  135. if (!DB::table('activity_rewards')->where('id', $activityId)->exists()) {
  136. throw ValidationException::withMessages(['link' => '绑定的活动不存在']);
  137. }
  138. }
  139. $manualStatus = $banner->status ?? 1;
  140. if ($activityId === null) {
  141. // Both the row button and the editor write status; a stale compatibility alias must not override it.
  142. $manualStatus = $params['status'] ?? $params['manual_status'] ?? $manualStatus;
  143. }
  144. $banner->fill([
  145. 'title' => $params['title'],
  146. 'image' => Util::replacePartInUrl($params['image']),
  147. 'click_type' => $links['click_type'],
  148. 'link' => $links['link'],
  149. 'link_pc' => $links['link_pc'],
  150. 'link_h5' => $links['link_h5'],
  151. 'type' => $params['type'],
  152. 'sort' => array_key_exists('sort', $params) ? ($params['sort'] ?? 0) : ($banner->sort ?? 0),
  153. 'lang' => $params['lang'],
  154. 'status' => $manualStatus,
  155. ])->save();
  156. DB::commit();
  157. } catch (ValidationException $e) {
  158. DB::rollBack();
  159. return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
  160. } catch (Exception $e) {
  161. DB::rollBack();
  162. return $this->error(intval($e->getCode()), $e->getMessage());
  163. }
  164. return $this->success();
  165. }
  166. /** Change manual publication without resubmitting the banner form. */
  167. public function status()
  168. {
  169. try {
  170. $params = request()->validate([
  171. 'id' => ['required', 'integer', 'min:1'],
  172. 'status' => ['required_without:manual_status', 'integer', 'in:0,1'],
  173. 'manual_status' => ['required_without:status', 'integer', 'in:0,1'],
  174. ]);
  175. DB::transaction(function () use ($params) {
  176. $banner = BannerModel::where('id', $params['id'])->lockForUpdate()->first();
  177. if (!$banner) throw new Exception('数据不存在', HttpStatus::CUSTOM_ERROR);
  178. if (BannerVisibilityService::boundActivityId($banner->click_type, $banner->link) !== null) {
  179. throw new Exception('关联活动的 Banner 由活动状态自动控制,不能手动上下架', HttpStatus::CUSTOM_ERROR);
  180. }
  181. $banner->status = $params['status'] ?? $params['manual_status'];
  182. $banner->save();
  183. });
  184. } catch (ValidationException $e) {
  185. return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
  186. } catch (Exception $e) {
  187. return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
  188. }
  189. return $this->success();
  190. }
  191. /**
  192. * @api {post} /admin/banner/delete 删除
  193. * @apiGroup 轮播图管理
  194. * @apiUse result
  195. * @apiUse header
  196. * @apiVersion 1.0.0
  197. *
  198. * @apiParam {int} id 要删除的ID
  199. *
  200. */
  201. function delete()
  202. {
  203. DB::beginTransaction();
  204. try {
  205. request()->validate([
  206. 'id' => ['required', 'integer', 'min:1', 'max:99999999'],
  207. ]);
  208. $id = request()->input('id', 0);
  209. $info = BannerModel::where('id', $id)->first();
  210. if (!$info) throw new Exception("数据不存在", HttpStatus::CUSTOM_ERROR);
  211. $info->delete();
  212. DB::commit();
  213. } catch (ValidationException $e) {
  214. DB::rollBack();
  215. return $this->error(HttpStatus::VALIDATION_FAILED, $e->validator->errors()->first());
  216. } catch (Exception $e) {
  217. DB::rollBack();
  218. return $this->error(intval($e->getCode()));
  219. }
  220. return $this->success();
  221. }
  222. }