diff --git a/app/Admin/Actions/Grid/DrawActivityClose.php b/app/Admin/Actions/Grid/DrawActivityClose.php new file mode 100644 index 00000000..d1316a1e --- /dev/null +++ b/app/Admin/Actions/Grid/DrawActivityClose.php @@ -0,0 +1,42 @@ + 关闭'; + } + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return $user->can('dcat.admin.draw_activities.close'); + } + + public function confirm() + { + return '您确定要关闭选中的抽奖活动吗?'; + } + + public function handle(Request $request) + { + $drawActivity = DrawActivity::findOrFail($this->getKey()); + + $drawActivity->update([ + 'status' => DrawActivityStatus::Closed, + ]); + + return $this->response()->success('操作成功')->refresh(); + } +} diff --git a/app/Admin/Actions/Grid/DrawActivityPrizeStockChange.php b/app/Admin/Actions/Grid/DrawActivityPrizeStockChange.php new file mode 100644 index 00000000..fe81c003 --- /dev/null +++ b/app/Admin/Actions/Grid/DrawActivityPrizeStockChange.php @@ -0,0 +1,36 @@ + 库存'; + } + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return $user->can('dcat.admin.draw_activities.prize_stock'); + } + + public function render() + { + $form = DrawActivityPrizeStockChangeForm::make()->payload(['id'=>$this->getKey()]); + + return Modal::make() + ->lg() + ->title($this->title()) + ->body($form) + ->button($this->title()); + } +} diff --git a/app/Admin/Actions/Grid/DrawActivityPublish.php b/app/Admin/Actions/Grid/DrawActivityPublish.php new file mode 100644 index 00000000..65bccdc8 --- /dev/null +++ b/app/Admin/Actions/Grid/DrawActivityPublish.php @@ -0,0 +1,36 @@ + 发布'; + } + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return $user->can('dcat.admin.draw_activities.publish'); + } + + public function render() + { + $form = DrawActivityPublishForm::make()->payload(['id'=>$this->getKey()]); + + return Modal::make() + ->lg() + ->title($this->title()) + ->body($form) + ->button($this->title()); + } +} diff --git a/app/Admin/Actions/Grid/DrawLogComplete.php b/app/Admin/Actions/Grid/DrawLogComplete.php new file mode 100644 index 00000000..d5e29f36 --- /dev/null +++ b/app/Admin/Actions/Grid/DrawLogComplete.php @@ -0,0 +1,45 @@ + 发放奖品'; + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return $user->can('dcat.admin.draw_activities.log_complete'); + } + + // 确认弹窗信息 + public function confirm() + { + return '您确定要发放中奖奖品吗?'; + } + + // 处理请求 + public function handle(Request $request) + { + $drawLog = DrawLog::findOrFail($this->getKey()); + + if (! $drawLog->isPending()) { + return $this->response()->error('操作失败:中奖记录状态异常')->refresh(); + } + + $drawLog->update([ + 'status' => DrawLogStatus::Completed, + ]); + + return $this->response()->success('操作成功')->refresh(); + } +} diff --git a/app/Admin/Actions/Show/DrawActivityPublish.php b/app/Admin/Actions/Show/DrawActivityPublish.php new file mode 100644 index 00000000..3704aeb5 --- /dev/null +++ b/app/Admin/Actions/Show/DrawActivityPublish.php @@ -0,0 +1,41 @@ + 发布'; + + /** + * @var string + */ + protected $style = 'btn-danger'; + + /** + * @param Model|Authenticatable|HasPermissions|null $user + * + * @return bool + */ + protected function authorize($user): bool + { + return $user->can('dcat.admin.draw_activities.publish'); + } + + public function render() + { + $form = DrawActivityPublishForm::make()->payload(['id'=>$this->getKey()]); + + return Modal::make() + ->lg() + ->title($this->title()) + ->body($form) + ->button("style}\">{$this->title}  "); + } +} diff --git a/app/Admin/Controllers/DrawActivityController.php b/app/Admin/Controllers/DrawActivityController.php new file mode 100644 index 00000000..90e180d7 --- /dev/null +++ b/app/Admin/Controllers/DrawActivityController.php @@ -0,0 +1,214 @@ +isClosed()) { + throw new BizException('活动已结束'); + } + + return parent::update($id); + } + + public function destroy($id) + { + $drawActivity = DrawActivity::findOrFail($id); + + if ($drawActivity->isPublished()) { + throw new BizException('活动已发布'); + } + + return Admin::json() + ->alert() + ->status(true) + ->message(trans('admin.delete_succeeded')); + } + + /** + * Make a grid builder. + * + * @return Grid + */ + protected function grid() + { + return Grid::make(new DrawActivityRepository(), function (Grid $grid) { + // 设置弹窗的宽和高 + $grid->setDialogFormDimensions('70%', '90%'); + + $grid->model()->orderBy('id', 'desc'); + + $grid->column('id')->sortable(); + $grid->column('name'); + $grid->column('start_at')->display(function ($v) { + return $v?->toDateTimeString() ?: '-'; + }); + $grid->column('end_at')->display(function ($v) { + return $v?->toDateTimeString() ?: '-'; + }); + $grid->column('published_at')->display(function ($v) { + return $v?->toDateTimeString() ?: '-'; + })->sortable(); + $grid->column('real_status')->display(function ($status) { + return $status->label(); + }); + $grid->column('bg_image')->image(null, 80, 80); + $grid->column('bg_color')->display(function () { + return $this->getBgColorLabel(); + }); + + if (Admin::user()->can('dcat.admin.draw_activities.create')) { + $grid->enableDialogCreate(); + $grid->showCreateButton(); + } + + $grid->actions(function (Grid\Displayers\Actions $actions) { + if (! $actions->row->isClosed() && Admin::user()->can('dcat.admin.draw_activities.edit')) { + $actions->quickEdit(true); + } + + if (Admin::user()->can('dcat.admin.draw_activities.show')) { + $actions->append(' 显示'); + } + + if (! $actions->row->isClosed() && $actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.close')) { + $actions->append(new DrawActivityClose()); + } + + if (! $actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.publish')) { + $actions->append(new DrawActivityPublish()); + } + + if ($actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.ticket_list')) { + $actions->append(' 抽奖次数'); + } + + if ($actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.log_list')) { + $actions->append(' 中奖记录'); + } + + if (! $actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.destroy')) { + $actions->disableDelete(false); + } + }); + + $grid->filter(function (Grid\Filter $filter) { + $filter->panel(false); + $filter->like('name')->width(3); + $filter->where('status', function ($builder) { + return match (DrawActivityStatus::tryFrom($this->input)) { + DrawActivityStatus::Created => $builder->onlyCreated(), + DrawActivityStatus::Publishing => $builder->onlyPublishing(), + DrawActivityStatus::Unstart => $builder->onlyUnstart(), + DrawActivityStatus::Running => $builder->onlyRunning(), + DrawActivityStatus::Closed =>$builder->onlyClosed(), + default => $builder, + }; + })->select(DrawActivityStatus::options())->width(3); + }); + }); + } + + protected function detail($id) + { + $drawActivity = DrawActivity::findOrFail($id); + + $row = new Row(); + + $row->column(5, Show::make($id, $drawActivity, function (Show $show) { + $show->field('name'); + $show->field('desc')->unescape(); + $show->field('start_at'); + $show->field('end_at'); + $show->field('real_status')->unescape()->as(function () { + return $this->real_status->label(); + }); + $show->field('published_at'); + $show->field('bg_image')->image(); + $show->field('bg_color')->unescape()->as(function () { + return $this->getBgColorLabel(); + }); + + $show->panel() + ->tools(function (Show\Tools $tools) use ($show) { + $tools->disableEdit(); + $tools->disableDelete(); + + if (! $show->model()->isPublished() && Admin::user()->can('dcat.admin.draw_activities.publish')) { + $tools->append(new DrawActivityPublishShowAction()); + } + }); + })); + + $row->column(7, tap(new Tab(), function (Tab $tab) use ($drawActivity) { + if (request()->routeIs('dcat.admin.draw_activities.ticket_list')) { + $tab->add('抽奖次数', new DrawActivityTicketTable(['draw_activity_id' => $drawActivity->id]), true); + } elseif (request()->routeIs('dcat.admin.draw_activities.log_list')) { + $tab->add('中奖记录', new DrawLogTable(['draw_activity_id' => $drawActivity->id]), true); + } else { + $tab->add('活动奖品', new DrawActivityPrizeTable(['draw_activity_id' => $drawActivity->id]), true); + } + + $tab->withCard(); + })); + + return $row; + } + + protected function form() + { + return Form::make(new DrawActivityRepository(), function (Form $form) { + $form->text('name') + ->rules(['bail', 'required', 'string', 'max:255']) + ->setLabelClass(['asterisk']) + ->attribute('required', true); + $form->editor('desc') + ->height('600'); + + if ($form->isEditing() && $form->model()->isPublished()) { + $form->display('start_at'); + $form->display('end_at'); + } else { + $form->datetime('start_at'); + $form->datetime('end_at'); + } + + $form->image('bg_image') + ->move('draw/activities') + ->uniqueName() + ->saveFullUrl() + ->removable(false) + ->autoUpload() + ->retainable(); + $form->text('bg_color')->help('十六进制颜色码'); + + $form->submitted(function (Form $form) { + if ($form->isEditing() && $form->model()->isPublished()) { + $form->deleteInput(['start_at', 'end_at']); + } + }); + }); + } +} diff --git a/app/Admin/Controllers/DrawActivityPrizeController.php b/app/Admin/Controllers/DrawActivityPrizeController.php new file mode 100644 index 00000000..1a0a4cdd --- /dev/null +++ b/app/Admin/Controllers/DrawActivityPrizeController.php @@ -0,0 +1,213 @@ +title; + } + + return admin_trans_label(); + } + + protected function description() + { + if (property_exists($this, 'description')) { + return $this->description; + } + } + + protected function translation() + { + if (property_exists($this, 'translation')) { + return $this->translation; + } + } + + public function create($drawActivityId, Content $content) + { + $drawActivity = DrawActivity::findOrFail($drawActivityId); + + return $content + ->translation($this->translation()) + ->title($this->title()) + ->description($this->description()['create'] ?? trans('admin.create')) + ->body($this->form($drawActivity)); + } + + public function store($drawActivityId) + { + $drawActivity = DrawActivity::findOrFail($drawActivityId); + + if ($drawActivity->isPublished()) { + throw new BizException('活动已发布'); + } + + return $this->form($drawActivity)->store(); + } + + public function edit($drawActivityId, $drawActivityPrizeId, Content $content) + { + $drawActivity = DrawActivity::findOrFail($drawActivityId); + + return $content + ->translation($this->translation()) + ->title($this->title()) + ->description($this->description()['edit'] ?? trans('admin.edit')) + ->body($this->form($drawActivity)->edit($drawActivityPrizeId)); + } + + public function update($drawActivityId, $drawActivityPrizeId) + { + $drawActivity = DrawActivity::findOrFail($drawActivityId); + + if ($drawActivity->isClosed()) { + throw new BizException('活动已结束'); + } + + $drawActivityPrize = $drawActivity->prizes()->findOrFail($drawActivityPrizeId); + + return $this->form($drawActivity)->update($drawActivityPrize->id); + } + + public function destroy($drawActivityId, $drawActivityPrizeId) + { + $drawActivity = DrawActivity::findOrFail($drawActivityId); + + if ($drawActivity->isPublished()) { + throw new BizException('活动已发布'); + } + + $drawActivity->prizes()->where('id', $drawActivityPrizeId)->delete(); + + return Admin::json() + ->alert() + ->status(true) + ->message(trans('admin.delete_succeeded')) + ->refresh(); + } + + protected function form(DrawActivity $drawActivity) + { + return Form::make(new DrawActivityPrizeRepository(), function (Form $form) use ($drawActivity) { + if ($form->isCreating()) { + $form->hidden('draw_activity_id')->default($drawActivity->id); + } + + if ($form->isCreating() || ! $drawActivity->isPublished()) { + $form->radio('draw_prize_source', '来源') + ->options([ + 1 => '自定义', + 2 => '奖品库', + ]) + ->when(1, function (Form $form) { + $form->text('name', '名称') + ->rules(['bail', 'required_if:draw_prize_source,1', 'nullable', 'string', 'max:255'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + + $form->image('icon', '图标') + ->move('draw/prizes') + ->uniqueName() + ->saveFullUrl() + ->removable(false) + ->autoUpload() + ->retainable() + ->rules(['bail', 'required_if:draw_prize_source,1'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + + $form->select('type', '类型') + ->options(DrawPrizeType::options()) + ->rules(['bail', 'required_if:draw_prize_source,1', 'nullable', Rule::in(array_keys(DrawPrizeType::options()))], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + + $form->text('amount', '面值/数量') + ->rules(['bail', 'required_if:draw_prize_source,1', 'nullable', 'min:0', 'regex:/^([1-9]\d*|0)(\.\d{1,2})?$/'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']) + ->help('精确到2位小数'); + }) + ->when(2, function (Form $form) { + $form->select('draw_prize_id', '奖品') + ->options(DrawPrize::pluck('name', 'id')) + ->rules(['bail', 'required_if:draw_prize_source,2'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + }) + ->setLabelClass(['asterisk']) + ->value($form->isEditing() ? 1 : 2) + ->default(1); + + $form->radio('limited', '是否限量') + ->options([ + 0 => '否', + 1 => '是', + ]) + ->when(1, function (Form $form) { + $form->number('stock', '库存') + ->min(0) + ->rules(['required_if:limited,1', 'int', 'min:0'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + }) + ->default(1) + ->customFormat(function ($v) { + return $v ? 1 : 0; + }) + ->setLabelClass(['asterisk']); + } else { + $form->display('name', '名称') + ->setLabelClass(['asterisk']); + $form->image('icon', '图标') + ->disable() + ->setLabelClass(['asterisk']); + $form->display('type', '类型') + ->setLabelClass(['asterisk']) + ->with(fn () => $this->type->label()); + $form + ->display('amount', '面值/数量') + ->setLabelClass(['asterisk']) + ->with(fn ($value) => trim_trailing_zeros($value)); + } + + $form->number('weight', '权重') + ->rules(['bail', 'required', 'int', 'min:0']) + ->min(0) + ->setLabelClass(['asterisk']) + ->attribute('required', true); + + $form->number('sort', '排序')->default(0)->rules(['required', 'int']); + + $form->saving(function (Form $form) use ($drawActivity) { + if (! $form->isCreating()) { + $form->deleteInput('draw_activity_id'); + } + + if ($form->isCreating() || ! $drawActivity->isPublished()) { + if ($form->input('draw_prize_source') != 1) { + $drawPrize = DrawPrize::findOrFail($form->input('draw_prize_id')); + + $form->input('name', $drawPrize->name); + $form->input('icon', $drawPrize->icon); + $form->input('type', $drawPrize->type); + $form->input('amount', $drawPrize->amount); + } + + $form->deleteInput(['draw_prize_source', 'draw_prize_id']); + } else { + $form->deleteInput(['name', 'icon', 'type', 'amount', 'limited', 'stock']); + } + }); + }); + } +} diff --git a/app/Admin/Controllers/DrawLogController.php b/app/Admin/Controllers/DrawLogController.php new file mode 100644 index 00000000..11c70da8 --- /dev/null +++ b/app/Admin/Controllers/DrawLogController.php @@ -0,0 +1,74 @@ +title; + } + + return admin_trans_label(); + } + + protected function description() + { + if (property_exists($this, 'description')) { + return $this->description; + } + } + + protected function translation() + { + if (property_exists($this, 'translation')) { + return $this->translation; + } + } + + public function edit($drawActivityId, $drawLogId, Content $content) + { + DrawActivity::findOrFail($drawActivityId); + + return $content + ->translation($this->translation()) + ->title($this->title()) + ->description($this->description()['edit'] ?? trans('admin.edit')) + ->body($this->form()->edit($drawLogId)); + } + + public function update($drawActivityId, $drawLogId) + { + DrawActivity::findOrFail($drawActivityId); + + return $this->form()->update($drawLogId); + } + + protected function form() + { + return Form::make(new DrawLogRepository(), function (Form $form) { + if ($form->isEditing()) { + if ($form->model()->isPending()) { + $form->text('consignee_name', '收件人') + ->rules(['bail', 'nullable', 'string', 'max:255']); + + $form->text('consignee_phone', '联系方式') + ->rules(['bail', 'nullable', 'string', 'max:255']); + + $form->text('consignee_address', '收货地址') + ->rules(['bail', 'nullable', 'string', 'max:255']); + } + + $form->textarea('remark', '备注') + ->rules(['bail', 'nullable', 'string', 'max:255']); + } + }); + } +} diff --git a/app/Admin/Controllers/DrawPrizeController.php b/app/Admin/Controllers/DrawPrizeController.php new file mode 100644 index 00000000..e687db23 --- /dev/null +++ b/app/Admin/Controllers/DrawPrizeController.php @@ -0,0 +1,97 @@ +column('id')->sortable(); + $grid->column('name'); + $grid->column('icon')->image(null, 80, 80); + $grid->column('type', '类型')->display(function ($type) { + return $type->label(); + }); + $grid->column('amount')->display(function ($v) { + return trim_trailing_zeros($v); + }); + $grid->column('created_at'); + $grid->column('updated_at'); + + $grid->model()->orderBy('id', 'asc'); + + if (Admin::user()->can('dcat.admin.draw_prizes.create')) { + $grid->enableDialogCreate(); + $grid->showCreateButton(); + } + + if (Admin::user()->can('dcat.admin.draw_prizes.edit')) { + $grid->showQuickEditButton(); + } + + if (Admin::user()->can('dcat.admin.draw_prizes.destroy')) { + $grid->showDeleteButton(); + } + + $grid->filter(function (Grid\Filter $filter) { + $filter->panel(false); + $filter->like('name')->width(3); + $filter->equal('type')->select(DrawPrizeType::options())->width(3); + }); + }); + } + + /** + * Make a form builder. + * + * @return Form + */ + protected function form() + { + return Form::make(new DrawPrizeRepository(), function (Form $form) { + $form->display('id'); + + $form->text('name') + ->rules(['bail', 'required', 'string', 'max:255']) + ->setLabelClass(['asterisk']) + ->attribute('required', true); + + $form->image('icon') + ->move('draw/prizes') + ->uniqueName() + ->saveFullUrl() + ->removable(false) + ->autoUpload() + ->retainable() + ->setLabelClass(['asterisk']) + ->rules(['bail', 'required']) + ->attribute('required', true); + + $form->select('type') + ->options(DrawPrizeType::options()) + ->rules(['bail', 'required', Rule::in(array_keys(DrawPrizeType::options()))]) + ->setLabelClass(['asterisk']) + ->attribute('required', true); + + $form->text('amount') + ->rules(['bail', 'required', 'min:0', 'regex:/^([1-9]\d*|0)(\.\d{1,2})?$/']) + ->setLabelClass(['asterisk']) + ->attribute('required', true) + ->help('最多保留2位小数'); + }); + } +} diff --git a/app/Admin/Forms/DrawActivityPrizeStockChange.php b/app/Admin/Forms/DrawActivityPrizeStockChange.php new file mode 100644 index 00000000..16d43210 --- /dev/null +++ b/app/Admin/Forms/DrawActivityPrizeStockChange.php @@ -0,0 +1,119 @@ +can('dcat.admin.draw_activities.prize_stock'); + } + + /** + * Handle the form request. + * + * @param array $input + * + * @return mixed + */ + public function handle(array $input) + { + $drawActivityPrize = DrawActivityPrize::findOrFail($this->payload['id']); + + if ($drawActivityPrize->activity->isClosed()) { + throw new BizException('活动已结束'); + } + + $limited = (bool) $input['limited']; + + try { + if ($limited) { + $stock = (int) $input['stock']; + + // 如果限量 + if ($drawActivityPrize->limited) { + if ($stock === 0) { + throw new BizException('库存不能为0'); + } + + $drawActivityPrize->increment('stock', $stock); + } else { + $drawActivityPrize->update([ + 'limited' => $limited, + 'stock' => $stock, + ]); + } + } else { + $drawActivityPrize->update([ + 'limited' => $limited, + 'stock' => 0, + ]); + } + } catch (QueryException $e) { + if (strpos($e->getMessage(), 'Numeric value out of range') !== false) { + $e = new BizException('奖品库存不足'); + } + + throw $e; + } + + return $this->response() + ->success(__('admin.update_succeeded')) + ->refresh(); + } + + /** + * Build a form here. + */ + public function form() + { + $drawActivityPrize = DrawActivityPrize::findOrFail($this->payload['id']); + + $this->radio('limited', '是否限量') + ->options([ + 0 => '否', + 1 => '是', + ]) + ->when(1, function () use ($drawActivityPrize) { + if ($drawActivityPrize->limited) { + $this->number('stock', '库存') + ->rules(['required_if:limited,1', 'int'], ['required_if' => '不能为空']) + ->help('库存大于0时,增加库存;小于0,减少库存') + ->setLabelClass(['asterisk']); + } else { + $this->number('stock', '初始库存') + ->min(0) + ->rules(['required_if:limited,1', 'int', 'min:0'], ['required_if' => '不能为空']) + ->setLabelClass(['asterisk']); + } + }) + ->value($drawActivityPrize->limited) + ->customFormat(function ($v) { + return $v ? 1 : 0; + }) + ->setLabelClass(['asterisk']); + } + + public function default() + { + return [ + ]; + } +} diff --git a/app/Admin/Forms/DrawActivityPublish.php b/app/Admin/Forms/DrawActivityPublish.php new file mode 100644 index 00000000..4b228205 --- /dev/null +++ b/app/Admin/Forms/DrawActivityPublish.php @@ -0,0 +1,106 @@ +can('dcat.admin.draw_activities.publish'); + } + + /** + * Handle the form request. + * + * @param array $input + * + * @return mixed + */ + public function handle(array $input) + { + $drawActivity = DrawActivity::findOrFail($this->payload['id']); + + if ($drawActivity->isPublished()) { + throw new BizException('活动已发布'); + } + + $method = (int) $input['method']; + + if ($method === static::METHOD_NONE) { + $drawActivity->update([ + 'status' => DrawActivityStatus::Created, + 'published_at' => null, + ]); + } else { + $prizes = $drawActivity->prizes()->get(); + + if ($prizes->isEmpty()) { + throw new BizException('活动未设置奖品'); + } + + if ($prizes->sum('weight') == 0) { + throw new BizException('活动奖品总权重值不能为0'); + } + + $drawActivity->update([ + 'status' => DrawActivityStatus::Running, + 'published_at' => match ($method) { + static::METHOD_TIMER => $input['published_at'], + default => now(), + }, + ]); + } + + return $this->response() + ->success(__('admin.update_succeeded')) + ->refresh(); + } + + /** + * Build a form here. + */ + public function form() + { + $this->radio('method', '发布方式') + ->options([ + static::METHOD_NONE => '暂不发布', + static::METHOD_QUICK => '立即发布', + static::METHOD_TIMER => '定时发布', + ]) + ->when(static::METHOD_TIMER, function () { + $this->datetime('published_at') + ->rules(['bail', 'required_if:method,'.static::METHOD_TIMER], ['required_if' => '不能为空']); + }); + } + + public function default() + { + $drawActivity = DrawActivity::findOrFail($this->payload['id']); + + return [ + 'method' => match ($drawActivity->real_status) { + DrawActivityStatus::Publishing => static::METHOD_TIMER, + default => static::METHOD_QUICK, + }, + 'published_at' => $drawActivity->published_at, + ]; + } +} diff --git a/app/Admin/Forms/DrawActivityTicketChange.php b/app/Admin/Forms/DrawActivityTicketChange.php new file mode 100644 index 00000000..c81dd1d5 --- /dev/null +++ b/app/Admin/Forms/DrawActivityTicketChange.php @@ -0,0 +1,118 @@ +can('dcat.admin.draw_activities.change_tickets'); + } + + /** + * Handle the form request. + * + * @param array $input + * + * @return mixed + */ + public function handle(array $input) + { + $drawActivity = DrawActivity::findOrFail($this->payload['id']); + + if ($drawActivity->isClosed()) { + throw new BizException('活动已结束'); + } + + $user = User::findOrFail($input['user_id']); + + switch ($input['type']) { + case static::TYPE_SUB: + $number = $input['number'] * -1; + $remark = $input['remark'] ?? '系统扣除'; + break; + + default: + $number = $input['number']; + $remark = $input['remark'] ?? '系统增加'; + break; + } + + try { + DB::beginTransaction(); + + (new DrawTicketService())->change($user, $drawActivity, $number, $remark); + + DB::commit(); + } catch (QueryException $e) { + DB::rollBack(); + + if (strpos($e->getMessage(), 'Numeric value out of range') !== false) { + throw new BizException('抽奖次数不足'); + } + + throw $e; + } catch (Throwable $e) { + DB::rollBack(); + + throw $e; + } + + return $this->response() + ->success(__('admin.update_succeeded')) + ->refresh(); + } + + /** + * Build a form here. + */ + public function form() + { + $this->radio('type', '类型') + ->options([ + static::TYPE_ADD => '增加', + static::TYPE_SUB => '扣除', + ]) + ->required(); + + $this->select('user_id', '手机号') + ->ajax(admin_route('api.users')) + ->required(); + + $this->number('number', '次数') + ->min(0) + ->required(); + + $this->textarea('remark', '备注') + ->rules(['max:255']); + } + + public function default() + { + return [ + 'type' => static::TYPE_ADD, + 'number' => 1, + ]; + } +} diff --git a/app/Admin/Renderable/DrawActivityPrizeTable.php b/app/Admin/Renderable/DrawActivityPrizeTable.php new file mode 100644 index 00000000..4ad8fba0 --- /dev/null +++ b/app/Admin/Renderable/DrawActivityPrizeTable.php @@ -0,0 +1,63 @@ +payload['draw_activity_id'] ?? 0); + + return Grid::make(new DrawActivityPrize(), function (Grid $grid) use ($drawActivity) { + $grid->setResource("draw-activities/{$drawActivity->id}/prizes"); + + $grid->model()->where('draw_activity_id', $drawActivity->id)->orderBy('sort', 'desc'); + + $grid->column('name', '名称'); + $grid->column('icon', '图标')->image(null, 80, 80); + $grid->column('type', '类型')->display(function ($type) { + return $type->label(); + }); + $grid->column('amount', '数量/面值')->display(function ($v) { + return trim_trailing_zeros($v); + }); + $grid->column('weight', '权重'); + $grid->column('stock', '库存')->display(function ($stock) { + if ($this->limited) { + return $stock; + } + + return '不限'; + }); + $grid->column('winnings', '中奖数量'); + $grid->column('sort', '排序'); + + $grid->disableRefreshButton(); + + if (! $drawActivity->isPublished() && Admin::user()->can('dcat.admin.draw_activities.prize_create')) { + $grid->enableDialogCreate(); + $grid->showCreateButton(); + } + + $grid->actions(function (Grid\Displayers\Actions $actions) use ($drawActivity) { + if (! $drawActivity->isClosed() && Admin::user()->can('dcat.admin.draw_activities.prize_edit')) { + $actions->quickEdit(true); + } + + if (! $drawActivity->isPublished() && Admin::user()->can('dcat.admin.draw_activities.prize_delete')) { + $actions->disableDelete(false); + } + if (! $drawActivity->isClosed() && $drawActivity->isPublished() && Admin::user()->can('dcat.admin.draw_activities.prize_stock')) { + $actions->append(new DrawActivityPrizeStockChange()); + } + }); + }); + } +} diff --git a/app/Admin/Renderable/DrawActivityTicketTable.php b/app/Admin/Renderable/DrawActivityTicketTable.php new file mode 100644 index 00000000..6909a0fa --- /dev/null +++ b/app/Admin/Renderable/DrawActivityTicketTable.php @@ -0,0 +1,40 @@ +payload['draw_activity_id'] ?? 0); + + return Grid::make(DrawTicket::with(['user', 'userInfo']), function (Grid $grid) use ($drawActivity) { + $grid->model()->where('draw_activity_id', $drawActivity->id)->orderBy('id', 'desc'); + + $grid->column('user.phone', '手机号'); + $grid->column('userInfo.nickname', '昵称'); + $grid->column('number', '次数')->sortable(); + + $grid->disableRefreshButton(); + $grid->disableActions(); + + + if (! $drawActivity->isClosed() && Admin::user()->can('dcat.admin.draw_activities.change_tickets')) { + $grid->tools(new DrawActivityTicketChange($drawActivity)); + } + + $grid->filter(function (Grid\Filter $filter) { + $filter->panel(); + $filter->like('user.phone', '手机号')->width(3); + $filter->like('userInfo.nickname', '昵称')->width(3); + }); + }); + } +} diff --git a/app/Admin/Renderable/DrawLogTable.php b/app/Admin/Renderable/DrawLogTable.php new file mode 100644 index 00000000..f3b2cfe2 --- /dev/null +++ b/app/Admin/Renderable/DrawLogTable.php @@ -0,0 +1,64 @@ +payload['draw_activity_id'] ?? 0); + + return Grid::make(DrawLog::with(['prize', 'user']), function (Grid $grid) use ($drawActivity) { + $grid->setResource("draw-activities/{$drawActivity->id}/logs"); + + $grid->model()->where('draw_activity_id', $drawActivity->id)->orderBy('id', 'desc'); + + $grid->column('prize.name', '奖品名称'); + $grid->column('prize.icon', '奖品图标')->image(null, 80, 80); + $grid->column('prize.type', '奖品类型')->display(function () { + return $this->prize->type->label(); + }); + $grid->column('user.phone', '中奖人手机号')->display(function () { + $href = admin_route('users.show', ['user' => $this->user_id]); + + return "{$this->user->phone}"; + }); + $grid->column('created_at', '中奖时间')->sortable(); + $grid->column('consignee_name', '收件人'); + $grid->column('consignee_phone', '联系方式'); + $grid->column('consignee_address', '收货地址'); + $grid->column('remark', '备注'); + $grid->column('status', '状态')->display(function () { + return $this->status->label(); + }); + + $grid->actions(function (Grid\Displayers\Actions $actions) { + if (Admin::user()->can('dcat.admin.draw_activities.log_edit')) { + $actions->quickEdit(true); + } + + if ($actions->row->isPending() && Admin::user()->can('dcat.admin.draw_activities.log_complete')) { + $actions->append(new DrawLogComplete()); + } + }); + + $grid->filter(function (Grid\Filter $filter) use ($drawActivity) { + $filter->panel(false); + $filter->like('user.phone', '手机号')->width(6); + $filter->equal('draw_activity_prize_id', '奖品名称')->select(DrawActivityPrize::where('draw_activity_id', $drawActivity->id)->pluck('name', 'id'))->width(6); + $filter->equal('prize.type', '奖品类型')->select(DrawPrizeType::options())->width(6); + $filter->equal('status', '状态')->select(DrawLogStatus::options())->width(6); + }); + }); + } +} diff --git a/app/Admin/Repositories/DrawActivity.php b/app/Admin/Repositories/DrawActivity.php new file mode 100644 index 00000000..b9d22c6f --- /dev/null +++ b/app/Admin/Repositories/DrawActivity.php @@ -0,0 +1,16 @@ +get('api/product-by-store', 'Store\ProductController@listByStore')->name('api.store_product'); $router->get('api/store', 'Store\StoreController@list')->name('api.store'); + // 抽奖管理 + $router->resource('draw-prizes', 'DrawPrizeController')->names('draw_prizes'); + $router->resource('draw-activities', 'DrawActivityController')->names('draw_activities'); + $router->get('draw-activities/{draw_activity}/prizes/create', 'DrawActivityPrizeController@create')->name('draw_activities.prize_create'); + $router->post('draw-activities/{draw_activity}/prizes', 'DrawActivityPrizeController@store')->name('draw_activities.prize_store'); + $router->get('draw-activities/{draw_activity}/prizes/{prize}/edit', 'DrawActivityPrizeController@edit')->name('draw_activities.prize_edit'); + $router->put('draw-activities/{draw_activity}/prizes/{prize}', 'DrawActivityPrizeController@update')->name('draw_activities.prize_update'); + $router->delete('draw-activities/{draw_activity}/prizes/{prize}', 'DrawActivityPrizeController@destroy')->name('draw_activities.prize_delete'); + $router->get('draw-activities/{draw_activity}/tickets', 'DrawActivityController@show')->name('draw_activities.ticket_list'); + $router->get('draw-activities/{draw_activity}/logs', 'DrawActivityController@show')->name('draw_activities.log_list'); + $router->put('draw-activities/{draw_activity}/logs/{log}', 'DrawLogController@update')->name('draw_activities.log_update'); + $router->get('draw-activities/{draw_activity}/logs/{log}/edit', 'DrawLogController@edit')->name('draw_activities.log_edit'); + /** 调试接口 **/ // $router->get('test', 'HomeController@test'); diff --git a/app/Endpoint/Api/Http/Controllers/DrawActivityController.php b/app/Endpoint/Api/Http/Controllers/DrawActivityController.php new file mode 100644 index 00000000..fe501797 --- /dev/null +++ b/app/Endpoint/Api/Http/Controllers/DrawActivityController.php @@ -0,0 +1,81 @@ + function ($builder) { + $builder->latest('sort'); + }])->onlyPublished()->findOrFail($id); + + $drawTicket = null; + $drawLog = null; + + if ($user = $request->user()) { + // 当前用户的抽奖机会 + $drawTicket = DrawTicket::where('draw_activity_id', $drawActivity->id) + ->where('user_id', $user->id) + ->first(); + + // 当前用户未填写收货地址的实物奖品 + $drawLog = DrawLog::whereRelation('prize', 'type', DrawPrizeType::Goods->value) + ->where('draw_activity_id', $drawActivity->id) + ->where('user_id', $user->id) + ->whereNull('consignee_name') + ->first(); + } + + return response()->json([ + 'draw_activity' => DrawActivityResource::make($drawActivity), + 'draw_log_id' => $drawLog?->id, + 'draw_tickets_number' => (int) $drawTicket?->number, + ]); + } + + public function draw($id, Request $request, DrawActivityService $drawActivityService) + { + $drawActivity = DrawActivity::onlyPublished()->findOrFail($id); + + $lock = Cache::lock("draw_activity_{$drawActivity->id}", 5); + + $user = $request->user(); + + try { + $lock->block(3); + + DB::beginTransaction(); + + $drawLog = $drawActivityService->draw($drawActivity, $user); + + DB::commit(); + } catch (Throwable $e) { + DB::rollBack(); + + if ($e instanceof LockTimeoutException) { + $e = new BizException('抽奖人数较多,请稍后'); + } + + throw $e; + } finally { + $lock?->release(); + } + + return DrawLogResource::make($drawLog); + } +} diff --git a/app/Endpoint/Api/Http/Controllers/DrawLogController.php b/app/Endpoint/Api/Http/Controllers/DrawLogController.php new file mode 100644 index 00000000..577a4686 --- /dev/null +++ b/app/Endpoint/Api/Http/Controllers/DrawLogController.php @@ -0,0 +1,56 @@ +where('draw_activity_id', $drawActivityId) + ->latest('id') + ->simplePaginate($request->input('per_page')); + + return DrawLogResource::collection($drawLogs); + } + + public function update($drawActivityId, $drawLogId, Request $request) + { + $request->validate([ + 'consignee_name' => ['bail', 'required', 'string', 'max:255'], + 'consignee_phone' => ['bail', 'required', 'string', 'max:255'], + 'consignee_address' => ['bail', 'required', 'string', 'max:255'], + ], [], [ + 'consignee_name' => '收件人', + 'consignee_phone' => '联系方式', + 'consignee_address' => '收货地址', + ]); + + $drawLog = DrawLog::where([ + 'user_id' => $request->user()->id, + 'draw_activity_id' => $drawActivityId, + ])->findOrFail($drawLogId); + + if ($drawLog->prize->type !== DrawPrizeType::Goods) { + throw new BizException('奖品不是实物'); + } + + if (filled($drawLog->consignee_name)) { + throw new BizException('请联系客服修改收件人信息'); + } + + $drawLog->update($request->only([ + 'consignee_name', + 'consignee_phone', + 'consignee_address', + ])); + + return DrawLogResource::make($drawLog); + } +} diff --git a/app/Endpoint/Api/Http/Resources/DrawActivityPrizeResource.php b/app/Endpoint/Api/Http/Resources/DrawActivityPrizeResource.php new file mode 100644 index 00000000..0d59422d --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/DrawActivityPrizeResource.php @@ -0,0 +1,25 @@ + $this->id, + 'name' => $this->name, + 'icon' => $this->icon, + 'type' => $this->type, + 'amount' => trim_trailing_zeros($this->amount), + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/DrawActivityResource.php b/app/Endpoint/Api/Http/Resources/DrawActivityResource.php new file mode 100644 index 00000000..1608874b --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/DrawActivityResource.php @@ -0,0 +1,29 @@ + $this->id, + 'name' => $this->name, + 'desc' => $this->desc, + 'prizes' => DrawActivityPrizeResource::collection($this->whenLoaded('prizes')), + 'start_at' => $this->start_at?->toDateTimeString(), + 'end_at' => $this->end_at?->toDateTimeString(), + 'bg_image' => $this->bg_image, + 'bg_color' => $this->bg_color, + 'status' => $this->real_status, + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/DrawLogResource.php b/app/Endpoint/Api/Http/Resources/DrawLogResource.php new file mode 100644 index 00000000..7d415188 --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/DrawLogResource.php @@ -0,0 +1,24 @@ + $this->id, + 'user' => UserInfoSimpleResource::make($this->whenLoaded('userInfo')), + 'prize' => DrawActivityPrizeResource::make($this->whenLoaded('prize')), + 'created_at' => $this->created_at?->toDateTimeString(), + ]; + } +} diff --git a/app/Endpoint/Api/routes.php b/app/Endpoint/Api/routes.php index 5fab6928..a247dd81 100644 --- a/app/Endpoint/Api/routes.php +++ b/app/Endpoint/Api/routes.php @@ -90,6 +90,9 @@ Route::group([ Route::get('bargain-order/sku/{sku}', [\App\Endpoint\Api\Http\Controllers\BargainController::class, 'barginaOrderBySku']); Route::get('bargain-order/order/{order}', [\App\Endpoint\Api\Http\Controllers\BargainController::class, 'bargainOrderById']); + // 抽奖活动信息 + Route::get('draw-activities/{draw_activity}', [App\Endpoint\Api\Http\Controllers\DrawActivityController::class, 'show']); + Route::get('draw-activities/{draw_activity}/logs', [App\Endpoint\Api\Http\Controllers\DrawLogController::class, 'index']); //三方登录聚合 Route::group([ @@ -207,6 +210,10 @@ Route::group([ // 佣金 Route::apiResource('profit', \App\Endpoint\Api\Http\Controllers\ProfitController::class)->only(['index', 'show']); + + // 抽奖活动抽奖 + Route::post('draw-activities/{draw_activity}/draw', [App\Endpoint\Api\Http\Controllers\DrawActivityController::class, 'draw']); + Route::put('draw-activities/{draw_activity}/logs/{log}', [App\Endpoint\Api\Http\Controllers\DrawLogController::class, 'update']); }); // 微信小程序 diff --git a/app/Enums/DrawActivityStatus.php b/app/Enums/DrawActivityStatus.php new file mode 100644 index 00000000..a3f6dd9f --- /dev/null +++ b/app/Enums/DrawActivityStatus.php @@ -0,0 +1,41 @@ + 'primary', + static::Publishing => 'blue', + static::Unstart => 'danger', + static::Running => 'success', + static::Closed => 'gray', + }; + + $background = Admin::color()->get($color, $color); + + $name = static::options()[$this->value] ?? 'Unknown'; + + return "{$name}"; + } + + public static function options() + { + return [ + static::Created->value => '已创建', + static::Publishing->value => '发布中', + static::Unstart->value => '未开始', + static::Running->value => '进行中', + static::Closed->value => '已结束', + ]; + } +} diff --git a/app/Enums/DrawLogStatus.php b/app/Enums/DrawLogStatus.php new file mode 100644 index 00000000..cea384db --- /dev/null +++ b/app/Enums/DrawLogStatus.php @@ -0,0 +1,35 @@ + 'primary', + static::Queuing => 'pink', + static::Completed => 'success', + }; + + $background = Admin::color()->get($color, $color); + + $name = static::options()[$this->value] ?? '其它'; + + return "{$name}"; + } + + public static function options() + { + return [ + static::Pending->value => '待处理', + static::Queuing->value => '发放中', + static::Completed->value => '已完成', + ]; + } +} diff --git a/app/Enums/DrawPrizeType.php b/app/Enums/DrawPrizeType.php new file mode 100644 index 00000000..3d4d7132 --- /dev/null +++ b/app/Enums/DrawPrizeType.php @@ -0,0 +1,44 @@ + 'warning', + static::Goods => 'primary', + static::Wallet => 'blue1', + static::Balance => 'pink', + static::Point => 'success', + static::Money => 'danger', + }; + + $background = Admin::color()->get($color, $color); + + $name = static::options()[$this->value] ?? '其它'; + + return "{$name}"; + } + + public static function options() + { + return [ + self::None->value => '谢谢参与', + self::Goods->value => '实物', + self::Wallet->value => '可提', + self::Balance->value => '余额', + self::Point->value => '积分', + self::Money->value => '现金', + ]; + } +} diff --git a/app/Models/DrawActivity.php b/app/Models/DrawActivity.php new file mode 100644 index 00000000..5b6cf54a --- /dev/null +++ b/app/Models/DrawActivity.php @@ -0,0 +1,193 @@ + DrawActivityStatus::Created, + ]; + + /** + * @var array + */ + protected $casts = [ + 'start_at' => 'datetime', + 'end_at' => 'datetime', + 'published_at' => 'datetime', + 'status' => DrawActivityStatus::class, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'name', + 'cover', + 'desc', + 'start_at', + 'end_at', + 'published_at', + 'bg_image', + 'bg_color', + 'status', + ]; + + /** + * 仅查询已发布的活动 + */ + public function scopeOnlyPublished(Builder $builder): Builder + { + return $builder->where('status', '!=', DrawActivityStatus::Created)->where('published_at', '<=', now()); + } + + /** + * 仅查询已创建的活动 + */ + public function scopeOnlyCreated(Builder $builder): Builder + { + return $builder->where('status', DrawActivityStatus::Created); + } + + /** + * 仅查询发布中的活动 + */ + public function scopeOnlyPublishing(Builder $builder): Builder + { + return $builder->where('status', DrawActivityStatus::Running)->where('published_at', '>', now()); + } + + /** + * 仅查询未开始的活动 + */ + public function scopeOnlyUnstart(Builder $builder): Builder + { + $tz = now(); + + return $builder->where('status', DrawActivityStatus::Running) + ->where('published_at', '<=', $tz) + ->whereNotNull('start_at') + ->where('start_at', '>', $tz); + } + + /** + * 仅查询进行中的活动 + */ + public function scopeOnlyRunning(Builder $builder): Builder + { + $tz = now(); + + return $builder->where('status', DrawActivityStatus::Running) + ->where('published_at', '<=', $tz) + ->where(function ($builder) use ($tz) { + $builder->where(function ($builder) use ($tz) { + $builder->whereNull('start_at')->orWhere(function ($builder) use ($tz) { + $builder->whereNotNull('start_at')->where('start_at', '<=', $tz); + }); + })->where(function ($builder) use ($tz) { + $builder->whereNull('end_at')->orWhere(function ($builder) use ($tz) { + $builder->whereNotNull('end_at')->where('end_at', '>', $tz); + }); + }); + }); + } + + /** + * 仅查询已结束的活动 + */ + public function scopeOnlyClosed(Builder $builder): Builder + { + return $builder->where('status', DrawActivityStatus::Closed) + ->orWhere(function ($builder) { + $tz = now(); + + return $builder->where('status', '!=', DrawActivityStatus::Created) + ->where('published_at', '<=', $tz) + ->whereNotNull('end_at') + ->where('end_at', '<=', $tz); + }); + } + + /** + * 属于此活动的奖品 + */ + public function prizes() + { + return $this->hasMany(DrawActivityPrize::class, 'draw_activity_id'); + } + + /** + * 确认此活动是否已发布 + * + * @return bool + */ + public function isPublished(): bool + { + return in_array($this->real_status, [ + DrawActivityStatus::Unstart, + DrawActivityStatus::Running, + DrawActivityStatus::Closed, + ]); + } + + /** + * 确认此活动是否未开始 + * + * @return bool + */ + public function isUnstart(): bool + { + return $this->real_status === DrawActivityStatus::Unstart; + } + + /** + * 确认此活动是否已结束 + * + * @return bool + */ + public function isClosed(): bool + { + return $this->real_status === DrawActivityStatus::Closed; + } + + public function getBgColorLabel() + { + if ($this->bg_color) { + return "{$this->bg_color}"; + } + } + + /** + * 获取此活动的真实状态 + * + * @return DrawActivityStatus + */ + public function getRealStatusAttribute(): DrawActivityStatus + { + if ($this->status === DrawActivityStatus::Running) { + if (now()->lt($this->published_at)) { + return DrawActivityStatus::Publishing; + } + + if ($this->start_at && now()->lt($this->start_at)) { + return DrawActivityStatus::Unstart; + } + + if ($this->end_at && now()->gte($this->end_at)) { + return DrawActivityStatus::Closed; + } + } + + return $this->status; + } +} diff --git a/app/Models/DrawActivityPrize.php b/app/Models/DrawActivityPrize.php new file mode 100644 index 00000000..23eb2f27 --- /dev/null +++ b/app/Models/DrawActivityPrize.php @@ -0,0 +1,65 @@ + DrawPrizeType::None, + 'weight' => 0, + 'limited' => true, + 'sort' => 0, + ]; + + /** + * @var array + */ + protected $casts = [ + 'type' => DrawPrizeType::class, + 'limited' => 'bool', + ]; + + /** + * @var array + */ + protected $fillable = [ + 'draw_activity_id', + 'name', + 'icon', + 'type', + 'amount', + 'weight', + 'limited', + 'stock', + 'winnings', + 'sort', + ]; + + public static function booted() + { + static::updating(function ($drawActivityPrize) { + if ($drawActivityPrize->weight == 0 && $drawActivityPrize->activity->isPublished()) { + $weight = $drawActivityPrize->activity->prizes()->where('id', '!=', $drawActivityPrize->id)->sum('weight'); + + if ($weight == 0) { + throw new BizException('活动奖品总权重值不能为0'); + } + } + }); + } + + public function activity() + { + return $this->belongsTo(DrawActivity::class, 'draw_activity_id'); + } +} diff --git a/app/Models/DrawLog.php b/app/Models/DrawLog.php new file mode 100644 index 00000000..9c26a972 --- /dev/null +++ b/app/Models/DrawLog.php @@ -0,0 +1,71 @@ + DrawLogStatus::Pending, + ]; + + /** + * @var array + */ + protected $casts = [ + 'status' => DrawLogStatus::class, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'user_id', + 'draw_activity_id', + 'draw_activity_prize_id', + 'consignee_name', + 'consignee_phone', + 'consignee_address', + 'remark', + 'status', + ]; + + public function scopeQueuing(Builder $builder): Builder + { + return $builder->where('status', DrawLogStatus::Queuing); + } + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function userInfo() + { + return $this->belongsTo(UserInfo::class, 'user_id', 'user_id'); + } + + public function activity() + { + return $this->belongsTo(DrawActivity::class, 'draw_activity_id'); + } + + public function prize() + { + return $this->belongsTo(DrawActivityPrize::class, 'draw_activity_prize_id'); + } + + public function isPending() + { + return $this->status === DrawLogStatus::Pending; + } +} diff --git a/app/Models/DrawPrize.php b/app/Models/DrawPrize.php new file mode 100644 index 00000000..9ec6e85f --- /dev/null +++ b/app/Models/DrawPrize.php @@ -0,0 +1,36 @@ + DrawPrizeType::None, + ]; + + /** + * @var array + */ + protected $casts = [ + 'type' => DrawPrizeType::class, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'name', + 'icon', + 'type', + 'amount', + ]; +} diff --git a/app/Models/DrawTicket.php b/app/Models/DrawTicket.php new file mode 100644 index 00000000..a6fe2bbc --- /dev/null +++ b/app/Models/DrawTicket.php @@ -0,0 +1,37 @@ + 0, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'draw_activity_id', + 'user_id', + 'number', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function userInfo() + { + return $this->belongsTo(UserInfo::class, 'user_id'); + } +} diff --git a/app/Models/DrawTicketLog.php b/app/Models/DrawTicketLog.php new file mode 100644 index 00000000..92dbcf07 --- /dev/null +++ b/app/Models/DrawTicketLog.php @@ -0,0 +1,28 @@ + 0, + ]; + + /** + * @var array + */ + protected $fillable = [ + 'draw_activity_id', + 'user_id', + 'number', + 'remark', + ]; +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 63cc7904..9e81c38b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -56,6 +56,7 @@ class AppServiceProvider extends ServiceProvider 'balance_log' => \App\Models\BalanceLog::class, 'admin_users' => \App\Models\Admin\Administrator::class, 'user_vip' => \App\Models\UserVip::class, + 'draw_log' => \App\Models\DrawLog::class, ]); JsonResource::withoutWrapping(); diff --git a/app/Services/DrawActivityService.php b/app/Services/DrawActivityService.php new file mode 100644 index 00000000..e124dd69 --- /dev/null +++ b/app/Services/DrawActivityService.php @@ -0,0 +1,98 @@ +isPublished()) { + throw new BizException('抽奖活动未发布'); + } + + if ($drawActivity->isUnstart()) { + throw new BizException('抽奖活动未开始'); + } + + if ($drawActivity->isClosed()) { + throw new BizException('抽奖活动已结束'); + } + + (new DrawTicketService())->change($user, $drawActivity, -1, '活动抽奖'); + + $prize = $this->getDrawActivityPrize($drawActivity); + + // 如果限制了奖品数量,则需扣除奖品库存 + if ($prize->limited) { + $prize->update([ + 'stock' => DB::raw('stock-1'), + 'winnings' => DB::raw('winnings+1'), + ]); + } else { + $prize->increment('winnings', 1); + } + + $drawLog = DrawLog::create([ + 'user_id' => $user->id, + 'draw_activity_id' => $drawActivity->id, + 'draw_activity_prize_id' => $prize->id, + 'status' => match ($prize->type) { + DrawPrizeType::None => DrawLogStatus::Completed, + DrawPrizeType::Wallet, DrawPrizeType::Balance, DrawPrizeType::Point => DrawLogStatus::Queuing, + default => DrawLogStatus::Pending, + }, + ]); + + return $drawLog->setRelation('prize', $prize); + } + + /** + * @param \App\Models\DrawActivity $drawActivity + * @return \App\Models\DrawActivityPrize + */ + protected function getDrawActivityPrize(DrawActivity $drawActivity): DrawActivityPrize + { + $drawActivityPrizes = $drawActivity->prizes()->get(); + + // 过滤权重为0或库存不足的奖品 + $prizes = $drawActivityPrizes->filter(function ($item) { + if ($item->weight === 0 || ($item->limited && $item->stock === 0)) { + return false; + } + + return true; + }); + + $max = $prizes->sum('weight'); + + if ($max > 0) { + $rand = mt_rand($min = 1, $max); + + foreach ($prizes as $prize) { + if ($rand <= $prize->weight + $min) { + return $prize; + } + + $min += $prize->weight; + } + } + + throw new BizException('抽奖活动异常'); + } +} diff --git a/app/Services/DrawTicketService.php b/app/Services/DrawTicketService.php new file mode 100644 index 00000000..7b05edba --- /dev/null +++ b/app/Services/DrawTicketService.php @@ -0,0 +1,57 @@ + $drawActivity->id, + 'user_id' => $user->id, + ])->first(); + + if ($drawTicket === null || $drawTicket->number + $number < 0) { + throw new BizException('抽奖次数不足'); + } + } else { + $drawTicket = DrawTicket::firstOrCreate([ + 'draw_activity_id' => $drawActivity->id, + 'user_id' => $user->id, + ], [ + 'number' => 0, + ]); + } + + $drawTicket->increment('number', $number); + + DrawTicketLog::create([ + 'draw_activity_id' => $drawActivity->id, + 'user_id' => $user->id, + 'number' => $number, + 'remark' => $remark, + ]); + } +} diff --git a/database/migrations/2022_05_16_144845_create_draw_activities_table.php b/database/migrations/2022_05_16_144845_create_draw_activities_table.php new file mode 100644 index 00000000..6823c2e2 --- /dev/null +++ b/database/migrations/2022_05_16_144845_create_draw_activities_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('name')->comment('名称'); + $table->string('cover')->nullable()->comment('封面图'); + $table->text('desc')->nullable()->comment('描述'); + $table->timestamp('start_at')->nullable()->comment('开始时间'); + $table->timestamp('end_at')->nullable()->comment('结束时间'); + $table->timestamp('published_at')->nullable()->comment('发布时间'); + $table->string('bg_image')->nullable()->comment('背景图片'); + $table->string('bg_color')->nullable()->comment('背景颜色'); + $table->tinyInteger('status')->default(0)->comment('状态'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_activities'); + } +} diff --git a/database/migrations/2022_05_16_144919_create_draw_tickets_table.php b/database/migrations/2022_05_16_144919_create_draw_tickets_table.php new file mode 100644 index 00000000..dd8d8bdd --- /dev/null +++ b/database/migrations/2022_05_16_144919_create_draw_tickets_table.php @@ -0,0 +1,36 @@ +id(); + $table->unsignedBigInteger('draw_activity_id')->comment('抽奖活动ID'); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->unsignedInteger('number')->default(0)->comment('次数'); + $table->timestamps(); + + $table->unique(['draw_activity_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_tickets'); + } +} diff --git a/database/migrations/2022_05_17_093626_create_draw_prizes_table.php b/database/migrations/2022_05_17_093626_create_draw_prizes_table.php new file mode 100644 index 00000000..534bbccc --- /dev/null +++ b/database/migrations/2022_05_17_093626_create_draw_prizes_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('name')->comment('名称'); + $table->string('icon')->nullable()->comment('图标'); + $table->tinyInteger('type')->comment('类型'); + $table->unsignedDecimal('amount')->default(0)->comment('面值/数量'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_prizes'); + } +} diff --git a/database/migrations/2022_05_17_113925_create_draw_activity_prizes_table.php b/database/migrations/2022_05_17_113925_create_draw_activity_prizes_table.php new file mode 100644 index 00000000..88182340 --- /dev/null +++ b/database/migrations/2022_05_17_113925_create_draw_activity_prizes_table.php @@ -0,0 +1,41 @@ +id(); + $table->unsignedBigInteger('draw_activity_id')->comment('抽奖活动ID'); + $table->string('name')->comment('名称'); + $table->string('icon')->nullable()->comment('图标'); + $table->tinyInteger('type')->comment('类型'); + $table->unsignedDecimal('amount')->default(0)->comment('面值/数量'); + $table->unsignedInteger('weight')->default(0)->comment('权重'); + $table->boolean('limited')->default(true)->comment('是否限量'); + $table->unsignedInteger('stock')->default(0)->comment('奖品库存'); + $table->unsignedInteger('winnings')->default(0)->comment('中奖数量'); + $table->integer('sort')->default(0)->comment('排序'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_activity_prizes'); + } +} diff --git a/database/migrations/2022_05_23_143947_create_draw_ticket_logs_table.php b/database/migrations/2022_05_23_143947_create_draw_ticket_logs_table.php new file mode 100644 index 00000000..a48b672e --- /dev/null +++ b/database/migrations/2022_05_23_143947_create_draw_ticket_logs_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('draw_activity_id')->comment('抽奖活动ID'); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->integer('number')->default(0)->comment('次数'); + $table->string('remark')->nullable()->comment('备注'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_ticket_logs'); + } +} diff --git a/database/migrations/2022_05_25_102413_create_draw_logs_table.php b/database/migrations/2022_05_25_102413_create_draw_logs_table.php new file mode 100644 index 00000000..d0502b81 --- /dev/null +++ b/database/migrations/2022_05_25_102413_create_draw_logs_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('user_id')->comment('用户ID'); + $table->unsignedBigInteger('draw_activity_id')->comment('活动ID'); + $table->unsignedBigInteger('draw_activity_prize_id')->comment('活动奖品ID'); + $table->string('consignee_name')->nullable()->comment('收件人-姓名'); + $table->string('consignee_phone')->nullable()->comment('收件人-联系方式'); + $table->string('consignee_address')->nullable()->comment('收件人-地址'); + $table->string('remark')->nullable()->comment('备注'); + $table->tinyInteger('status')->default(0)->comment('状态'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('draw_logs'); + } +}