6
0
Fork 0

抽奖活动

release
Jing Li 2022-06-02 09:53:15 +08:00
parent c90a503775
commit b5a1d28226
45 changed files with 2555 additions and 0 deletions

View File

@ -0,0 +1,42 @@
<?php
namespace App\Admin\Actions\Grid;
use App\Enums\DrawActivityStatus;
use App\Models\DrawActivity;
use Dcat\Admin\Grid\RowAction;
use Illuminate\Http\Request;
class DrawActivityClose extends RowAction
{
public function title()
{
return '<i class="feather icon-x-circle grid-action-icon"></i> 关闭';
}
/**
* @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();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Admin\Actions\Grid;
use App\Admin\Forms\DrawActivityPrizeStockChange as DrawActivityPrizeStockChangeForm;
use Dcat\Admin\Grid\RowAction;
use Dcat\Admin\Widgets\Modal;
class DrawActivityPrizeStockChange extends RowAction
{
public function title()
{
return '<i class="feather icon-edit grid-action-icon"></i> 库存';
}
/**
* @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());
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Admin\Actions\Grid;
use App\Admin\Forms\DrawActivityPublish as DrawActivityPublishForm;
use Dcat\Admin\Grid\RowAction;
use Dcat\Admin\Widgets\Modal;
class DrawActivityPublish extends RowAction
{
public function title()
{
return '<i class="feather icon-navigation grid-action-icon"></i> 发布';
}
/**
* @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());
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Admin\Actions\Grid;
use App\Enums\DrawLogStatus;
use App\Models\DrawLog;
use Dcat\Admin\Grid\RowAction;
use Illuminate\Http\Request;
class DrawLogComplete extends RowAction
{
protected $title = '<i class="feather icon-navigation grid-action-icon"></i> 发放奖品';
/**
* @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();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Admin\Actions\Show;
use App\Admin\Forms\DrawActivityPublish as DrawActivityPublishForm;
use Dcat\Admin\Show\AbstractTool;
use Dcat\Admin\Widgets\Modal;
class DrawActivityPublish extends AbstractTool
{
/**
* @return string
*/
protected $title = '<i class="feather icon-navigation"></i>&nbsp;发布';
/**
* @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("<a href=\"javascript:void(0)\" class=\"btn btn-sm {$this->style}\">{$this->title}</a>&nbsp;&nbsp;");
}
}

View File

@ -0,0 +1,214 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Actions\Grid\DrawActivityClose;
use App\Admin\Actions\Grid\DrawActivityPublish;
use App\Admin\Actions\Show\DrawActivityPublish as DrawActivityPublishShowAction;
use App\Admin\Renderable\DrawActivityPrizeTable;
use App\Admin\Renderable\DrawActivityTicketTable;
use App\Admin\Renderable\DrawLogTable;
use App\Admin\Repositories\DrawActivity as DrawActivityRepository;
use App\Enums\DrawActivityStatus;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use Dcat\Admin\Admin;
use Dcat\Admin\Form;
use Dcat\Admin\Grid;
use Dcat\Admin\Http\Controllers\AdminController;
use Dcat\Admin\Layout\Row;
use Dcat\Admin\Show;
use Dcat\Admin\Widgets\Tab;
class DrawActivityController extends AdminController
{
public function update($id)
{
$drawActivity = DrawActivity::findOrFail($id);
if ($drawActivity->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('<a style="cursor: pointer;" target="_blank" href="'.admin_route('draw_activities.show', ['draw_activity' => $actions->row]).'"><i class="feather icon-eye"></i> 显示</a>');
}
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('<a style="cursor: pointer;" target="_blank" href="'.admin_route('draw_activities.ticket_list', ['draw_activity' => $actions->row]).'"><i class="feather icon-eye"></i> 抽奖次数</a>');
}
if ($actions->row->isPublished() && Admin::user()->can('dcat.admin.draw_activities.log_list')) {
$actions->append('<a style="cursor: pointer;" target="_blank" href="'.admin_route('draw_activities.log_list', ['draw_activity' => $actions->row]).'"><i class="feather icon-eye"></i> 中奖记录</a>');
}
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']);
}
});
});
}
}

View File

@ -0,0 +1,213 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Repositories\DrawActivityPrize as DrawActivityPrizeRepository;
use App\Enums\DrawPrizeType;
use App\Exceptions\BizException;
use App\Http\Controllers\Controller;
use App\Models\DrawActivity;
use App\Models\DrawPrize;
use Dcat\Admin\Admin;
use Dcat\Admin\Form;
use Dcat\Admin\Layout\Content;
use Illuminate\Validation\Rule;
class DrawActivityPrizeController extends Controller
{
protected function title()
{
if (property_exists($this, 'title')) {
return $this->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']);
}
});
});
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Repositories\DrawLog as DrawLogRepository;
use App\Http\Controllers\Controller;
use App\Models\DrawActivity;
use Dcat\Admin\Form;
use Dcat\Admin\Layout\Content;
class DrawLogController extends Controller
{
protected function title()
{
if (property_exists($this, 'title')) {
return $this->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']);
}
});
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Repositories\DrawPrize as DrawPrizeRepository;
use App\Enums\DrawPrizeType;
use Dcat\Admin\Admin;
use Dcat\Admin\Form;
use Dcat\Admin\Grid;
use Dcat\Admin\Http\Controllers\AdminController;
use Illuminate\Validation\Rule;
class DrawPrizeController extends AdminController
{
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
return Grid::make(new DrawPrizeRepository(), function (Grid $grid) {
$grid->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位小数');
});
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Admin\Forms;
use App\Exceptions\BizException;
use App\Models\DrawActivityPrize;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
use Illuminate\Database\QueryException;
class DrawActivityPrizeStockChange extends Form implements LazyRenderable
{
use LazyWidget;
public const TYPE_ADD = 1;
public const TYPE_SUB = 2;
/**
* @param Model|Authenticatable|HasPermissions|null $user
*
* @return bool
*/
protected function authorize($user): bool
{
return $user->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 [
];
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Admin\Forms;
use App\Enums\DrawActivityStatus;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
class DrawActivityPublish extends Form implements LazyRenderable
{
use LazyWidget;
public const METHOD_NONE = 1; // 暂不发布
public const METHOD_QUICK = 2; // 立即发布
public const METHOD_TIMER = 3; // 定时发布
/**
* @param Model|Authenticatable|HasPermissions|null $user
*
* @return bool
*/
protected function authorize($user): bool
{
return $user->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,
];
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Admin\Forms;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use App\Models\User;
use App\Services\DrawTicketService;
use Dcat\Admin\Contracts\LazyRenderable;
use Dcat\Admin\Traits\LazyWidget;
use Dcat\Admin\Widgets\Form;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Throwable;
class DrawActivityTicketChange extends Form implements LazyRenderable
{
use LazyWidget;
public const TYPE_ADD = 1;
public const TYPE_SUB = 2;
/**
* @param Model|Authenticatable|HasPermissions|null $user
*
* @return bool
*/
protected function authorize($user): bool
{
return $user->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,
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Admin\Renderable;
use App\Admin\Actions\Grid\DrawActivityPrizeStockChange;
use App\Admin\Repositories\DrawActivityPrize;
use App\Models\DrawActivity;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\LazyRenderable;
class DrawActivityPrizeTable extends LazyRenderable
{
public function grid(): Grid
{
$drawActivity = DrawActivity::findOrFail($this->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());
}
});
});
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Admin\Renderable;
use App\Admin\Extensions\Grid\Tools\DrawActivityTicketChange;
use App\Admin\Repositories\DrawTicket;
use App\Models\DrawActivity;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\LazyRenderable;
class DrawActivityTicketTable extends LazyRenderable
{
public function grid(): Grid
{
$drawActivity = DrawActivity::findOrFail($this->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);
});
});
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Admin\Renderable;
use App\Admin\Actions\Grid\DrawLogComplete;
use App\Enums\DrawLogStatus;
use App\Enums\DrawPrizeType;
use App\Models\DrawActivity;
use App\Models\DrawActivityPrize;
use App\Models\DrawLog;
use Dcat\Admin\Admin;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\LazyRenderable;
class DrawLogTable extends LazyRenderable
{
public function grid(): Grid
{
$drawActivity = DrawActivity::findOrFail($this->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 "<a href=\"{$href}\" target=\"_blank\">{$this->user->phone}</a>";
});
$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);
});
});
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Repositories;
use App\Models\DrawActivity as Model;
use Dcat\Admin\Repositories\EloquentRepository;
class DrawActivity extends EloquentRepository
{
/**
* Model.
*
* @var string
*/
protected $eloquentClass = Model::class;
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Repositories;
use App\Models\DrawActivityPrize as Model;
use Dcat\Admin\Repositories\EloquentRepository;
class DrawActivityPrize extends EloquentRepository
{
/**
* Model.
*
* @var string
*/
protected $eloquentClass = Model::class;
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Repositories;
use App\Models\DrawLog as Model;
use Dcat\Admin\Repositories\EloquentRepository;
class DrawLog extends EloquentRepository
{
/**
* Model.
*
* @var string
*/
protected $eloquentClass = Model::class;
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Repositories;
use App\Models\DrawPrize as Model;
use Dcat\Admin\Repositories\EloquentRepository;
class DrawPrize extends EloquentRepository
{
/**
* Model.
*
* @var string
*/
protected $eloquentClass = Model::class;
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Repositories;
use App\Models\DrawTicket as Model;
use Dcat\Admin\Repositories\EloquentRepository;
class DrawTicket extends EloquentRepository
{
/**
* Model.
*
* @var string
*/
protected $eloquentClass = Model::class;
}

View File

@ -169,6 +169,19 @@ Route::group([
$router->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');

View File

@ -0,0 +1,81 @@
<?php
namespace App\Endpoint\Api\Http\Controllers;
use App\Endpoint\Api\Http\Resources\DrawActivityResource;
use App\Endpoint\Api\Http\Resources\DrawLogResource;
use App\Enums\DrawPrizeType;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use App\Models\DrawLog;
use App\Models\DrawTicket;
use App\Services\DrawActivityService;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Throwable;
class DrawActivityController extends Controller
{
public function show($id, Request $request)
{
$drawActivity = DrawActivity::with(['prizes' => 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);
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Endpoint\Api\Http\Controllers;
use App\Endpoint\Api\Http\Resources\DrawLogResource;
use App\Enums\DrawPrizeType;
use App\Exceptions\BizException;
use App\Models\DrawLog;
use Illuminate\Http\Request;
class DrawLogController extends Controller
{
public function index($drawActivityId, Request $request)
{
$drawLogs = DrawLog::with(['userInfo', 'prize'])
->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);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Endpoint\Api\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class DrawActivityPrizeResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'icon' => $this->icon,
'type' => $this->type,
'amount' => trim_trailing_zeros($this->amount),
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Endpoint\Api\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class DrawActivityResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $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,
];
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Endpoint\Api\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class DrawLogResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'user' => UserInfoSimpleResource::make($this->whenLoaded('userInfo')),
'prize' => DrawActivityPrizeResource::make($this->whenLoaded('prize')),
'created_at' => $this->created_at?->toDateTimeString(),
];
}
}

View File

@ -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']);
});
// 微信小程序

View File

@ -0,0 +1,41 @@
<?php
namespace App\Enums;
use Dcat\Admin\Admin;
enum DrawActivityStatus: int {
case Created = 0; // 已创建
case Publishing = 1; // 发布中
case Unstart = 2; // 未开始
case Running = 3; // 进行中
case Closed = 4; // 已结束
public function label()
{
$color = match ($this) {
static::Created => '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 "<span class='label' style='background: $background;'>{$name}</span>";
}
public static function options()
{
return [
static::Created->value => '已创建',
static::Publishing->value => '发布中',
static::Unstart->value => '未开始',
static::Running->value => '进行中',
static::Closed->value => '已结束',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Enums;
use Dcat\Admin\Admin;
enum DrawLogStatus: int {
case Pending = 0;
case Queuing = 1;
case Completed = 5;
public function label()
{
$color = match ($this) {
static::Pending => 'primary',
static::Queuing => 'pink',
static::Completed => 'success',
};
$background = Admin::color()->get($color, $color);
$name = static::options()[$this->value] ?? '其它';
return "<span class='label' style='background: $background;'>{$name}</span>";
}
public static function options()
{
return [
static::Pending->value => '待处理',
static::Queuing->value => '发放中',
static::Completed->value => '已完成',
];
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Enums;
use Dcat\Admin\Admin;
enum DrawPrizeType: int {
case None = 0;
case Goods = 1;
case Wallet = 2;
case Balance = 3;
case Point = 4;
case Money = 5;
public function label()
{
$color = match ($this) {
static::None => '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 "<span class='label' style='background: $background;'>{$name}</span>";
}
public static function options()
{
return [
self::None->value => '谢谢参与',
self::Goods->value => '实物',
self::Wallet->value => '可提',
self::Balance->value => '余额',
self::Point->value => '积分',
self::Money->value => '现金',
];
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace App\Models;
use App\Enums\DrawActivityStatus;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class DrawActivity extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'status' => 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 "<span class='label shadow-100' style='background: {$this->bg_color};'>{$this->bg_color}</span>";
}
}
/**
* 获取此活动的真实状态
*
* @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;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Models;
use App\Enums\DrawPrizeType;
use App\Exceptions\BizException;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
class DrawActivityPrize extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'type' => 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');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Enums\DrawLogStatus;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class DrawLog extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'status' => 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;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Models;
use App\Enums\DrawPrizeType;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
class DrawPrize extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'type' => DrawPrizeType::None,
];
/**
* @var array
*/
protected $casts = [
'type' => DrawPrizeType::class,
];
/**
* @var array
*/
protected $fillable = [
'name',
'icon',
'type',
'amount',
];
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
class DrawTicket extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'number' => 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');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Dcat\Admin\Traits\HasDateTimeFormatter;
use Illuminate\Database\Eloquent\Model;
class DrawTicketLog extends Model
{
use HasDateTimeFormatter;
/**
* @var array
*/
protected $attributes = [
'number' => 0,
];
/**
* @var array
*/
protected $fillable = [
'draw_activity_id',
'user_id',
'number',
'remark',
];
}

View File

@ -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();

View File

@ -0,0 +1,98 @@
<?php
namespace App\Services;
use App\Enums\DrawLogStatus;
use App\Enums\DrawPrizeType;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use App\Models\DrawActivityPrize;
use App\Models\DrawLog;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class DrawActivityService
{
/**
* 活动抽奖
*
* @param \App\Models\DrawActivity $drawActivity
* @param \App\Models\User $user
* @return \App\Models\DrawLog
*/
public function draw(DrawActivity $drawActivity, User $user): DrawLog
{
if (! $drawActivity->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('抽奖活动异常');
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Services;
use App\Exceptions\BizException;
use App\Models\DrawActivity;
use App\Models\DrawTicket;
use App\Models\DrawTicketLog;
use App\Models\User;
class DrawTicketService
{
/**
* 增加/扣除 用户在给定活动中的抽奖机会
*
* @param \App\Models\User $user
* @param \App\Models\DrawActivity $drawActivity
* @param int $number
* @param string|null $remark
* @return void
*
* @throws \App\Exceptions\BizException
*/
public function change(User $user, DrawActivity $drawActivity, int $number, ?string $remark = null): void
{
if ($number === 0) {
throw new BizException('抽奖次数不能为 0');
}
if ($number < 0) {
$drawTicket = DrawTicket::where([
'draw_activity_id' => $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,
]);
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawActivitiesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_activities', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawTicketsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_tickets', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawPrizesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_prizes', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawActivityPrizesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_activity_prizes', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawTicketLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_ticket_logs', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDrawLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('draw_logs', function (Blueprint $table) {
$table->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');
}
}