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');
+ }
+}