Major update: 强化卡券玩法 & 相关代码优化

详情 https://proxypanel.gitbook.io/wiki/coupon
This commit is contained in:
兔姬桑
2022-09-20 20:36:11 +08:00
parent 3514d264be
commit 7010822acc
16 changed files with 784 additions and 220 deletions

View File

@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponRequest;
use App\Models\Coupon;
use App\Models\Level;
use App\Models\UserGroup;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -35,6 +37,16 @@ class CouponController extends Controller
return view('admin.coupon.index', ['couponList' => $query->latest()->paginate(15)->appends($request->except('page'))]);
}
// 优惠券列表
public function show(Coupon $coupon)
{
return view('admin.coupon.show', [
'coupon' => $coupon,
'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
'levels' => Level::all()->pluck('name', 'level')->toArray(),
]);
}
// 添加优惠券
public function store(CouponRequest $request)
{
@@ -49,7 +61,28 @@ class CouponController extends Controller
$logo = 'upload/'.$fileName;
}
$num = (int) $request->input('num');
$data = $request->only(['name', 'type', 'usable_times', 'value', 'rule', 'start_time', 'end_time']);
$data = $request->only(['name', 'type', 'priority', 'usable_times', 'value', 'start_time', 'end_time']);
$data['limit'] = [
'minimum' => $request->input('minimum'),
'used' => $request->input('used'),
'users' => [
'white' => $request->has('users_whitelist') ? array_map('intval', explode(', ', $request->input('users_whitelist'))) : null,
'black' => $request->has('users_blacklist') ? array_map('intval', explode(', ', $request->input('users_blacklist'))) : null,
'newbie' => [
'coupon' => $request->input('coupon'),
'order' => $request->input('order'),
'days' => $request->has('days') ? (int) $request->input(['days']) : null,
],
'levels' => $request->has('levels') ? array_map('intval', $request->input('levels')) : null,
'groups' => $request->has('groups') ? array_map('intval', $request->input('groups')) : null,
],
'services' => [
'white' => $request->has('services_whitelist') ? array_map('intval', explode(', ', $request->input('services_whitelist'))) : null,
'black' => $request->has('services_blacklist') ? array_map('intval', explode(', ', $request->input('services_blacklist'))) : null,
],
];
array_clean($data['limit']);
$data['logo'] = $logo;
$data['status'] = 0;
try {
@@ -69,7 +102,10 @@ class CouponController extends Controller
// 添加优惠券页面
public function create()
{
return view('admin.coupon.create');
return view('admin.coupon.create', [
'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
'levels' => Level::all()->pluck('name', 'level')->toArray(),
]);
}
// 删除优惠券
@@ -108,10 +144,10 @@ class CouponController extends Controller
$spreadsheet->setActiveSheetIndex(0);
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('抵用券');
$sheet->fromArray(['名称', '使用次数', '有效期', '券码', '金额(元)', '使用限制(元)']);
$sheet->fromArray(['名称', '使用次数', '有效期', '券码', '金额(元)', '权重', '使用限制']);
foreach ($voucherList as $k => $vo) {
$dateRange = $vo->start_time.' ~ '.$vo->end_time;
$sheet->fromArray([$vo->name, $vo->usable_times ?? '无限制', $dateRange, $vo->sn, $vo->value, $vo->rule], null, 'A'.($k + 2));
$sheet->fromArray([$vo->name, $vo->usable_times ?? '无限制', $dateRange, $vo->sn, $vo->value, $vo->priority, json_encode($vo->limit)], null, 'A'.($k + 2));
}
// 折扣券
@@ -119,10 +155,10 @@ class CouponController extends Controller
$spreadsheet->setActiveSheetIndex(1);
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('折扣券');
$sheet->fromArray(['名称', '使用次数', '有效期', '券码', '折扣(折)', '使用限制(元)']);
$sheet->fromArray(['名称', '使用次数', '有效期', '券码', '折扣(折)', '权重', '使用限制']);
foreach ($discountCouponList as $k => $vo) {
$dateRange = $vo->start_time.' ~ '.$vo->end_time;
$sheet->fromArray([$vo->name, $vo->usable_times ?? '无限制', $dateRange, $vo->sn, $vo->value, $vo->rule], null, 'A'.($k + 2));
$sheet->fromArray([$vo->name, $vo->usable_times ?? '无限制', $dateRange, $vo->sn, $vo->value, $vo->priority, json_encode($vo->limit)], null, 'A'.($k + 2));
}
// 充值券

View File

@@ -18,6 +18,7 @@ use App\Models\Coupon;
use App\Models\Goods;
use App\Models\Order;
use App\Models\Payment;
use App\Services\CouponService;
use Auth;
use Exception;
use Illuminate\Http\JsonResponse;
@@ -127,13 +128,12 @@ class PaymentController extends Controller
// 使用优惠券
if ($coupon_sn) {
$ret = $this->couponCheck($coupon_sn, $goods->price);
$ret = (new CouponService($coupon_sn))->search($goods); // 检查券合规性
if ($ret !== true) {
if (! $ret instanceof Coupon) {
return $ret;
}
$coupon = Coupon::whereStatus(0)->whereIn('type', [1, 2])->whereSn($coupon_sn)->firstOrFail();
$coupon = $ret;
// 计算实际应支付总价
$amount = $coupon->type === 2 ? $goods->price * $coupon->value / 100 : $goods->price - $coupon->value;
@@ -198,50 +198,6 @@ class PaymentController extends Controller
return Response::json(['status' => 'fail', 'message' => '订单创建失败']);
}
public function couponCheck($coupon_sn, $price) // 检查券合规性
{
if (empty($coupon_sn)) {
return Response::json([
'status' => 'fail', 'title' => trans('common.failed'), 'message' => trans('validation.required', ['attribute' => trans('user.coupon.attribute')]),
]);
}
$coupon = Coupon::whereSn($coupon_sn)->whereIn('type', [1, 2])->first();
if (! $coupon) {
return Response::json(['status' => 'fail', 'title' => trans('common.failed'), 'message' => trans('user.coupon.error.unknown')]);
}
if ($coupon->status === 1) {
return Response::json(['status' => 'fail', 'title' => trans('common.sorry'), 'message' => trans('user.coupon.error.used')]);
}
if ($coupon->getRawOriginal('end_time') < time()) {
$coupon->status = 2;
$coupon->save();
return Response::json(['status' => 'fail', 'title' => trans('common.sorry'), 'message' => trans('user.coupon.error.expired')]);
}
if ($coupon->status === 2) {
if ($coupon->usable_times === 0) {
return Response::json(['status' => 'fail', 'title' => trans('common.sorry'), 'message' => trans('user.coupon.error.run_out')]);
}
return Response::json(['status' => 'fail', 'title' => trans('common.sorry'), 'message' => trans('user.coupon.error.expired')]);
}
if ($coupon->start_time > date('Y-m-d H:i:s')) {
return Response::json(['status' => 'fail', 'title' => trans('user.coupon.error.inactive'),
'message' => trans('user.coupon.error.wait', ['time' => $coupon->start_time]),
]);
}
if ($price < $coupon->rule) {
return Response::json(['status' => 'fail', 'title' => trans('user.coupon.error.limit'), 'message' => trans('user.coupon.error.higher', ['amount' => $coupon->rule])]);
}
return true;
}
public function close(Order $order): JsonResponse
{
if (! $order->close()) {

View File

@@ -14,6 +14,7 @@ use App\Models\Ticket;
use App\Models\User;
use App\Notifications\TicketCreated;
use App\Notifications\TicketReplied;
use App\Services\CouponService;
use Cache;
use DB;
use Exception;
@@ -205,14 +206,11 @@ class UserController extends Controller
if ($user && $nodes = $user->userGroup) {
$nodes = $nodes->nodes();
foreach ($goodsList as $goods) {
$goods->node_count = $nodes->where('level', '<=', $goods->level)->count();
}
} else {
$nodes = Node::all();
foreach ($goodsList as $goods) {
$goods->node_count = $nodes->where('level', '<=', $goods->level)->count();
}
}
foreach ($goodsList as $goods) {
$goods->node_count = $nodes->where('level', '<=', $goods->level)->count();
}
return view('user.services', [
@@ -385,14 +383,12 @@ class UserController extends Controller
if ($user->invite_num <= 0) {
return Response::json(['status' => 'fail', 'message' => trans('user.invite.generate_failed')]);
}
$obj = new Invite();
$obj->inviter_id = $user->id;
$obj->code = strtoupper(mb_substr(md5(microtime().Str::random()), 8, 12));
$obj->dateline = date('Y-m-d H:i:s', strtotime(sysConfig('user_invite_days').' days'));
$obj->save();
if ($obj) {
$user->update(['invite_num' => $user->invite_num - 1]);
$invite = $user->invites()->create([
'code' => strtoupper(mb_substr(md5(microtime().Str::random()), 8, 12)),
'dateline' => date('Y-m-d H:i:s', strtotime(sysConfig('user_invite_days').' days')),
]);
if ($invite) {
$user->decrement('invite_num');
return Response::json(['status' => 'success', 'message' => trans('common.generate_item', ['attribute' => trans('common.success')])]);
}
@@ -401,21 +397,24 @@ class UserController extends Controller
}
// 使用优惠券
public function redeemCoupon(Request $request): JsonResponse
public function redeemCoupon(Request $request, Goods $good): JsonResponse
{
$coupon_sn = $request->input('coupon_sn');
$ret = (new PaymentController())->couponCheck($coupon_sn, $request->input('price'));
if (empty($coupon_sn)) {
return Response::json(['status' => 'fail', 'title' => trans('common.failed'), 'message' => trans('user.coupon.error.unknown')]);
}
if ($ret !== true) {
$ret = (new CouponService($coupon_sn))->search($good); // 检查券合规性
if (! $ret instanceof Coupon) {
return $ret;
}
$coupon = Coupon::whereSn($coupon_sn)->whereIn('type', [1, 2])->firstOrFail();
$data = [
'name' => $coupon->name,
'type' => $coupon->type,
'value' => $coupon->value,
'name' => $ret->name,
'type' => $ret->type,
'value' => $ret->value,
];
return Response::json(['status' => 'success', 'data' => $data, 'message' => trans('common.applied', ['attribute' => trans('user.coupon.attribute')])]);
@@ -534,32 +533,10 @@ class UserController extends Controller
return Response::json(['status' => 'fail', 'message' => $validator->errors()->all()]);
}
$coupon = Coupon::whereSn($request->input('coupon_sn'))->firstOrFail();
try {
DB::beginTransaction();
// 写入日志
$user = auth()->user();
Helpers::addUserCreditLog($user->id, null, $user->credit, $user->credit + $coupon->value, $coupon->value,
trans('user.recharge').' - ['.trans('user.coupon.recharge').''.$request->input('coupon_sn').']');
// 余额充值
$user->updateCredit($coupon->value);
// 更改卡券状态
$coupon->update(['status' => 1]);
// 写入卡券日志
Helpers::addCouponLog(trans('user.recharge_credit'), $coupon->id);
DB::commit();
if ((new CouponService($request->input('coupon_sn')))->charge()) {
return Response::json(['status' => 'success', 'message' => trans('user.recharge').trans('common.success')]);
} catch (Exception $e) {
Log::error(trans('user.recharge').trans('common.failed').$e->getMessage());
DB::rollBack();
return Response::json(['status' => 'fail', 'message' => trans('user.recharge').trans('common.failed')]);
}
return Response::json(['status' => 'fail', 'message' => trans('user.recharge').trans('common.failed')]);
}
}

View File

@@ -10,12 +10,23 @@ class CouponRequest extends FormRequest
{
return [
'name' => 'required|string',
'sn' => 'unique:coupon',
'sn' => 'exclude_unless:type,3|unique:coupon',
'logo' => 'nullable|image',
'type' => 'required|numeric|between:1,3',
'usable_times' => 'nullable|numeric',
'priority' => 'nullable|numeric|min:0|max:255',
'usable_times' => 'nullable|numeric|min:1',
'value' => 'required|numeric|min:0',
'rule' => 'nullable|numeric',
'minimum' => 'nullable|numeric',
'used' => 'nullable|numeric',
'levels' => 'nullable|array',
'groups' => 'nullable|array',
'users_whitelist' => 'nullable|string',
'users_blacklist' => 'nullable|string',
'services_blacklist' => 'nullable|string',
'services_whitelist' => 'nullable|string',
'coupon' => 'nullable',
'order' => 'nullable',
'days' => 'nullable|numeric',
'num' => 'required|numeric|min:1',
'start_time' => 'required|date|before_or_equal:end_time',
'end_time' => 'required|date|after_or_equal:start_time',

View File

@@ -13,7 +13,7 @@ class Coupon extends Model
use SoftDeletes;
protected $table = 'coupon';
protected $casts = ['start_time' => 'date:Y-m-d', 'end_time' => 'date:Y-m-d'];
protected $casts = ['limit' => 'array', 'start_time' => 'date:Y-m-d', 'end_time' => 'date:Y-m-d'];
protected $dates = ['deleted_at'];
protected $guarded = [];
@@ -32,4 +32,18 @@ class Coupon extends Model
{
return $this->attributes['end_time'] = strtotime($value);
}
public function used()
{
$this->attributes['status'] = 1;
return $this->save();
}
public function expired()
{
$this->attributes['status'] = 2;
return $this->save();
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Services;
use App\Components\Helpers;
use App\Models\Coupon;
use App\Models\Goods;
use Auth;
use Exception;
use Illuminate\Support\Facades\Log;
use Response;
class CouponService
{
public $code;
public $user;
public function __construct(string $code)
{
$this->code = $code;
$this->user = Auth::getUser();
}
public function search(Goods $goods) // 寻找合适的券
{
$coupons = Coupon::whereSn($this->code)->whereIn('type', [1, 2])->orderByDesc('priority')->get();
if ($coupons->isNotEmpty()) {
foreach ($coupons as $coupon) {
$ret = $this->check($goods, $coupon);
if ($ret === true) { // passed
return $coupon;
}
}
return $ret ?? $this->failedReturn(trans('common.failed'), trans('user.coupon.error.unknown'));
}
return $this->failedReturn(trans('common.failed'), trans('user.coupon.error.unknown'));
}
private function check(Goods $goods, Coupon $coupon) // 检查券合规性
{
if ($coupon->status === 1) {
return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.used'));
}
if (time() > $coupon->getRawOriginal('end_time')) {
$coupon->expired();
return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.expired'));
}
if ($coupon->usable_times === 0) {
return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.run_out'));
}
if ($coupon->status === 2) {
return $this->failedReturn(trans('common.sorry'), trans('user.coupon.error.expired'));
}
if (time() < $coupon->getRawOriginal('start_time')) {
return $this->failedReturn(trans('user.coupon.error.inactive'), trans('user.coupon.error.wait', ['time' => $coupon->start_time]));
}
if (isset($coupon->limit['minimum']) && $goods->price < $coupon->limit['minimum']) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.minimum', ['amount' => $coupon->limit['minimum']]));
}
if (isset($coupon->limit['users']['black']) && in_array($this->user->id, $coupon->limit['users']['black'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['services']['black']) && in_array($goods->id, $coupon->limit['services']['black'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.services'));
}
if (isset($coupon->limit['users']['white']) && ! in_array($this->user->id, $coupon->limit['users']['white'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['services']['white']) && ! in_array($goods->id, $coupon->limit['services']['white'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.services'));
}
if (isset($coupon->limit['users']['levels']) && ! in_array($this->user->level, $coupon->limit['users']['levels'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['users']['groups']) && ! in_array($this->user->user_group_id, $coupon->limit['users']['groups'], true)) {
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['users']['newbie'])) { // 新用户可用
if (isset($coupon->limit['users']['newbie']['coupon']) && $this->user->orders()->whereNotNull('coupon_id')->exists()) { // 第一次使用优惠券
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['users']['newbie']['order']) && $this->user->orders()->exists()) { // 第一个套餐订单
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
if (isset($coupon->limit['users']['newbie']['days']) && (time() > strtotime($this->user->created_at.' +'.$coupon->limit['users']['newbie']['days'].' days') ||
$this->user->orders()->whereCouponId($coupon->id)->exists())) { // 创号N天内, 且第一次用优惠券
return $this->failedReturn(trans('user.coupon.error.unmet'), trans('user.coupon.error.users'));
}
}
if (isset($coupon->limit['used']) && $this->user->orders()->whereCouponId($coupon->id)->count() >= $coupon->limit['used']) {
return $this->failedReturn(trans('user.coupon.error.unmet'),
trans_choice('user.coupon.error.overused', $coupon->limit['used'], ['times' => $coupon->limit['used']]));
}
return true;
}
private function failedReturn(string $title, string $message)
{
return Response::json(['status' => 'fail', 'title' => $title, 'message' => $message]);
}
public function charge(): bool
{
$coupon = Coupon::whereSn($this->code)->whereType(3)->first();
if ($coupon && $coupon->status === 0) {
try {
Helpers::addUserCreditLog($this->user->id, null, $this->user->credit, $this->user->credit + $coupon->value, $coupon->value,
trans('user.recharge').' - ['.trans('user.coupon.recharge').''.$coupon->sn.']'); // 写入用户余额变动日志
$this->user->updateCredit($coupon->value); // 余额充值
$coupon->used(); // 更改卡券状态
Helpers::addCouponLog(trans('user.recharge_credit'), $coupon->id); // 写入卡券使用日志
return true;
} catch (Exception $exception) {
Log::emergency('[重置券处理出现错误] '.$exception->getMessage());
}
}
return false;
}
}

View File

@@ -109,3 +109,20 @@ if (! function_exists('string_decrypt')) {
return openssl_decrypt(base64url_decode($data), 'aes-128-ctr', hash('sha256', config('app.key')), OPENSSL_RAW_DATA, substr(sha1(config('app.key')), 0, 16));
}
}
// Array values and indexes clean
if (! function_exists('array_clean')) {
function array_clean(array &$array): array
{
foreach ($array as $key => &$value) {
if (is_array($value)) {
$value = array_clean($value);
}
if (empty($value)) {
unset($array[$key]);
}
}
return $array;
}
}

View File

@@ -0,0 +1,59 @@
<?php
use App\Components\MigrationToolBox;
use App\Models\Coupon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ImproveCoupon extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('coupon', function (Blueprint $table) {
if ((new MigrationToolBox())->versionCheck()) {
$table->json('limit')->nullable()->comment('使用限制')->after('rule');
} else {
$table->text('limit')->nullable()->comment('使用限制')->after('rule');
}
$table->unsignedTinyInteger('priority')->default(0)->comment('使用权重, 高者优先')->after('limit');
$table->dropUnique(['sn']);
});
foreach (Coupon::whereNotNull('rule')->get() as $coupon) {
$coupon->update(['limit' => ['minimum' => $coupon->rule]]);
}
Schema::table('coupon', function (Blueprint $table) {
$table->dropColumn('rule');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('coupon', function (Blueprint $table) {
$table->unsignedInteger('rule')->nullable()->comment('使用限制(元)')->after('limit');
});
foreach (Coupon::whereNotNull('limit')->get() as $coupon) {
if (isset($coupon->limit['minimum'])) {
$coupon->update(['rule' => $coupon->limit['minimum']]);
}
}
Schema::table('coupon', function (Blueprint $table) {
$table->dropColumn('limit', 'priority');
$table->unique('sn');
});
}
}

View File

@@ -96,8 +96,11 @@ return [
'run_out' => 'Run Out of Usage',
'inactive' => 'Coupon Inactive yet',
'wait' => 'The Event will begin until :time, Please wait',
'limit' => 'Order\'s price is reach the minimum requirement of this coupon',
'higher' => 'The minimum requirement of this coupon is ¥:amount',
'unmet' => 'Conditions of use are not met',
'minimum' => 'The minimum requirement of this coupon is ¥:amount',
'overused' => 'You can only use this coupon once|You can only use this coupon :times times',
'users' => 'This account is not eligible for promotions',
'services' => 'This item is not qualify for this discount, please check the promotion terms',
],
],
'error_response' => 'Something went wrong, please try again later.',

View File

@@ -96,8 +96,11 @@ return [
'run_out' => '优惠券耗尽',
'inactive' => '优惠券尚未生效',
'wait' => '活动将于:time生效请耐心等待',
'limit' => '使用条件未满足',
'higher' => '本券最低使用金额为 ¥:amount',
'unmet' => '使用条件未满足',
'minimum' => '本券最低使用金额为 ¥:amount',
'overused' => '本券只能使用 :times 次',
'users' => '帐户不符合促销条件',
'services' => '商品不符合折扣条件,请查看促销条款',
],
],
'error_response' => '出现了错误,请稍后再试。',
@@ -119,7 +122,7 @@ return [
],
'referral' => [
'link' => '推广链接',
'total' => '合计返利 :amount :total 次),满 :money 可以申请提现。',
'total' => '合计返利 ¥ :amount :total 次),满 ¥ :money 可以申请提现。',
'amount' => '消费金额',
'commission' => '返利金额',
'logs' => '佣金记录',
@@ -128,7 +131,7 @@ return [
'msg' => [
'account' => '账号已过期,请先购买服务吧',
'applied' => '已存在申请,请等待之前的申请处理完',
'unfulfilled' => '满:amount才可以提现,继续努力吧',
'unfulfilled' => '满¥ :amount才可以提现继续努力吧',
'wait' => '请等待管理员审核',
'error' => '返利单建立失败,请稍后尝试或通知管理员',
],

View File

@@ -1,7 +1,9 @@
@extends('admin.layouts')
@section('css')
<link href="/assets/global/vendor/dropify/dropify.min.css" rel="stylesheet">
<link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
<link href="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.css" rel="stylesheet">
<link href="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.css" rel="stylesheet">
@endsection
@section('content')
<div class="page-content container">
@@ -58,33 +60,143 @@
<span class="text-help"> 抵用:抵扣商品金额,折扣:商品百分比打折,充值:充值用户账号余额 </span>
</div>
</div>
<div class="form-group row usage">
<label class="col-md-2 col-form-label" for="usable_times">使用次数</label>
<div class="col-md-4 input-group">
<input type="number" class="form-control" name="usable_times" id="usable_times" value="{{old('usable_times')}}"/>
<span class="input-group-text"></span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="value">优惠额度</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control col-md-3" name="value" id="value" value="{{old('value')}}" required/>
<input type="number" class="form-control col-md-3" min="1" max="99" name="value" id="value" value="{{old('value')}}" required/>
<span class="input-group-text" id="amount"></span>
<span class="input-group-text discount" style="display: none;">%</span>
</div>
<span class="text-help discount" style="display: none;"> 范围为 1~99折,即 1% ~ 99% </span>
<span class="text-help discount" style="display: none;"> 范围为 1% ~ 99% </span>
</div>
</div>
<div class="form-group row usage">
<label class="col-md-2 col-form-label" for="rule">满减条件</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control col-md-3" name="rule" id="rule" value="{{old('rule')}}" step="0.01" />
<span class="input-group-text"></span>
<div class="usage">
<div class="form-group row">
<label class="col-md-2 col-form-label" for="priority"> </label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control col-md-2" min="0" max="255" name="priority" id="priority" value="{{old('priority')}}"/>
</div>
<span class="text-help"> 同【使用券码】下,符合条件的高权重码将会被优先使用。最高为 255 </span>
</div>
<span class="text-help"> 当支付金额超过N值时才能使用本优惠劵不设置/0,即为无限制 </span>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="usable_times">使用次数</label>
<div class="col-md-4 input-group">
<input type="number" class="form-control" min="1" name="usable_times" id="usable_times" value="{{old('usable_times', 1)}}"/>
<span class="input-group-text"></span>
</div>
</div>
<hr>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="minimum">满减条件</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control col-md-3" name="minimum" id="minimum" value="{{old('minimum')}}" step="0.01"/>
<span class="input-group-text"></span>
</div>
<span class="text-help"> 当支付金额超过N值时才能使用本优惠劵不设置/0,即为无限制 </span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="used">个人限用</label>
<div class="col-md-10">
<div class="input-group">
<input type="number" class="form-control col-md-3" name="used" id="used" value="{{old('used')}}" step="1"/>
<span class="input-group-text"></span>
</div>
<span class="text-help"> 符合条件的用户可以使用本券N次不设置/0,即为无限制 </span>
</div>
</div>
<div class="form-group row">
<label for="levels" class="col-md-2 col-form-label">等级限定</label>
<div class="col-md-10">
<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="levels" name="levels[]"
multiple>
@foreach($levels as $key => $level)
<option value="{{$key}}">{{$level}}</option>
@endforeach
</select>
<span class="text-help"> 用户等级在选定等级内,方可使用本券</span>
</div>
</div>
<div class="form-group row">
<label for="groups" class="col-md-2 col-form-label">分组限定</label>
<div class="col-md-10">
<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="groups" name="groups[]"
multiple>
@foreach($userGroups as $key => $group)
<option value="{{$key}}">{{$group}}</option>
@endforeach
</select>
<span class="text-help"> 选定的用户分组,方可使用本券</span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="users_whitelist">专属用户</label>
<div class="col-md-10">
<input type="text" class="form-control col-md-6" data-plugin="tokenfield" id="users_whitelist" name="users_whitelist"
value="{{old('users_whitelist')}}" placeholder="输入用户ID, 再回车"/>
<span class="text-help"> 涉及用户均可使用本券,留空为不使用此条件</span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="users_blacklist">禁用用户</label>
<div class="col-md-10">
<input type="text" class="form-control col-md-6" data-plugin="tokenfield" id="users_blacklist" name="users_blacklist"
value="{{old('users_blacklist')}}" placeholder="输入用户ID, 再回车"/>
<span class="text-help"> 涉及用户均不可使用本券,空为不使用此条件</span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="services_whitelist">许可商品</label>
<div class="col-md-10">
<input type="text" class="form-control col-md-4" data-plugin="tokenfield" id="services_whitelist" name="services_whitelist"
value="{{old('services_whitelist')}}" placeholder="输入商品ID, 再回车"/>
<span class="text-help"> 涉及商品方可使用本券,留空为不使用此条件</span>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="services_blacklist">禁用商品</label>
<div class="col-md-10">
<input type="text" class="form-control col-md-4" data-plugin="tokenfield" id="services_blacklist" name="services_blacklist"
value="{{old('services_blacklist')}}" placeholder="输入商品ID, 再回车"/>
<span class="text-help"> 涉及商品不可使用本券,留空为不使用此条件</span>
</div>
</div>
<div class="form-group row">
<label for="newbie" class="col-md-2 col-form-label">新人专属</label>
<div class="col-md-10">
<ul class="list-unstyled">
<li class="list-group-item p-0">
<div class="checkbox-custom checkbox-primary">
<input type="checkbox" id="coupon" name="coupon" {{ old('coupon') ? 'checked' : '' }}/>
<label for="coupon">首次用任意券</label>
</div>
</li>
<li class="list-group-item p-0">
<div class="checkbox-custom checkbox-primary">
<input type="checkbox" id="order" name="order" {{ old('order') ? 'checked' : '' }}/>
<label for="order">首单</label>
</div>
</li>
<li class="list-group-item pb-0 pl-0">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text" for="days">创号</label>
</div>
<input type="number" class="form-control col-md-3" name="days" id="days" value="{{old('days')}}"/>
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</li>
</ul>
<span class="text-help"> 本项各条件为 <strong>并且</strong> 关系,请自行搭配使用 </span>
</div>
</div>
<hr>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="num">数量</label>
@@ -119,38 +231,41 @@
</div>
@endsection
@section('javascript')
<script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
<script src="/assets/global/vendor/dropify/dropify.min.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
<script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
<script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
<script src="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.js"></script>
<script src="/assets/global/js/Plugin/dropify.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-tokenfield.js"></script>
<script>
@if(old('type'))
@if(old())
$(document).ready(function() {
$("input[name='type'][value='{{old('type')}}']").click();
$("input[name='type'][value='{{old('type')}}']").click();
$('#levels').selectpicker('val', @json(old('levels')));
$('#groups').selectpicker('val', @json(old('groups')));
});
@endif
$('.input-daterange>input').datepicker({
format: 'yyyy-mm-dd',
format: 'yyyy-mm-dd',
});
$('input[name=\'type\']').change(function() {
if ($(this).val() === '2') {
$('#rule').attr('required', true);
$('.discount').show();
$('.usage').show();
$('.amount').hide();
} else if ($(this).val() === '3') {
$('#rule').attr('required', false);
$('.discount').hide();
$('.usage').hide();
$('.amount').show();
} else {
$('#rule').attr('required', true);
$('.discount').hide();
$('.usage').show();
$('.amount').show();
}
if ($(this).val() === '2') {
$('.discount').show();
$('.usage').show();
$('#amount').hide();
} else if ($(this).val() === '3') {
$('.discount').hide();
$('.usage').hide();
$('#amount').show();
} else {
$('.discount').hide();
$('.usage').show();
$('#amount').show();
}
});
</script>
@endsection

View File

@@ -52,6 +52,7 @@
<th> 券码</th>
<th> 图片</th>
<th> 类型</th>
<th> 权重</th>
<th> 使用次数</th>
<th> 优惠</th>
<th> 有效期</th>
@@ -67,36 +68,32 @@
<td> {{$coupon->sn}} </td>
<td> @if($coupon->logo) <img src="{{asset($coupon->logo)}}" class="h-50" alt="优惠码logo"/> @endif </td>
<td>
@if($coupon->type === 1)
抵用券
@elseif($coupon->type === 2)
折扣券
@else
充值券
@endif
{{ ['未知卡券','抵用券','折扣券','充值券'][$coupon->type] }}
</td>
<td> {{$coupon->priority}} </td>
<td> {{$coupon->type === 3 ? '一次性' : ($coupon->usable_times ?? '无限制')}} </td>
<td>
{{$coupon->value}}@if($coupon->type === 2)%@else元@endif
{{($coupon->type === 2 ?'减 ':'抵 ').$coupon->value.($coupon->type === 2 ?' %':' 元')}}
</td>
<td> {{$coupon->start_time}} ~ {{$coupon->end_time}} </td>
<td>
@if($coupon->status === 1)
<span class="badge badge-lg badge-default"> 已使用 </span>
@elseif ($coupon->status === 2)
<span class="badge badge-lg badge-default"> 已失效 </span>
@else
<span class="badge badge-lg badge-success"> 生效中 </span>
@endif
<span class="badge badge-lg @if($coupon->status) badge-default @else badge-success @endif"> {{['生效中','已使用','已失效'][$coupon->status]}} </span>
</td>
<td>
@if($coupon->status !== 1)
@can('admin.coupon.destroy')
<button class="btn btn-danger" onclick="delCoupon('{{$coupon->id}}','{{$coupon->name}}')">
<i class="icon wb-close"></i>
</button>
<div class="btn-group">
@can('admin.coupon.show')
<a class="btn btn-info" href="{{route('admin.coupon.show', $coupon)}}" target="_blank">
<i class="icon wb-eye"></i>
</a>
@endcan
@endif
@if($coupon->status !== 1)
@can('admin.coupon.destroy')
<button class="btn btn-danger" onclick="delCoupon('{{$coupon->id}}','{{$coupon->name}}')">
<i class="icon wb-close"></i>
</button>
@endcan
@endif
</div>
</td>
</tr>
@endforeach
@@ -122,59 +119,59 @@
<script src="/assets/global/vendor/bootstrap-table/bootstrap-table.min.js"></script>
<script src="/assets/global/vendor/bootstrap-table/extensions/mobile/bootstrap-table-mobile.min.js"></script>
<script>
$(document).ready(function() {
$('#type').val({{Request::query('type')}});
$('#status').val({{Request::query('status')}});
$(document).ready(function() {
$('#type').val({{Request::query('type')}});
$('#status').val({{Request::query('status')}});
$('select').on('change', function() { this.form.submit(); });
$('select').on('change', function() { this.form.submit(); });
});
@can('admin.coupon.export')
// 批量导出卡券
function exportCoupon() {
swal.fire({
title: '卡券导出',
text: '确定导出所有卡券吗?',
icon: 'question',
showCancelButton: true,
cancelButtonText: '{{trans('common.close')}}',
confirmButtonText: '{{trans('common.confirm')}}',
}).then((result) => {
if (result.value) {
window.location.href = '{{route('admin.coupon.export')}}';
}
});
}
@endcan
@can('admin.coupon.export')
// 批量导出卡券
function exportCoupon() {
swal.fire({
title: '卡券导出',
text: '确定导出所有卡券吗?',
icon: 'question',
showCancelButton: true,
cancelButtonText: '{{trans('common.close')}}',
confirmButtonText: '{{trans('common.confirm')}}',
}).then((result) => {
if (result.value) {
window.location.href = '{{route('admin.coupon.export')}}';
@can('admin.coupon.destroy')
// 删除卡券
function delCoupon(id, name) {
swal.fire({
title: '确定删除卡券 【' + name + '】 吗?',
icon: 'question',
allowEnterKey: false,
showCancelButton: true,
cancelButtonText: '{{trans('common.close')}}',
confirmButtonText: '{{trans('common.confirm')}}',
}).then((result) => {
if (result.value) {
$.ajax({
method: 'DELETE',
url: '{{route('admin.coupon.destroy', '')}}/' + id,
data: {_token: '{{csrf_token()}}'},
dataType: 'json',
success: function(ret) {
if (ret.status === 'success') {
swal.fire({title: ret.message, icon: 'success', timer: 1000, showConfirmButton: false}).then(() => window.location.reload());
} else {
swal.fire({title: ret.message, icon: 'error'}).then(() => window.location.reload());
}
},
});
}
@endcan
@can('admin.coupon.destroy')
// 删除卡券
function delCoupon(id, name) {
swal.fire({
title: '确定删除卡券 【' + name + '】 吗?',
icon: 'question',
allowEnterKey: false,
showCancelButton: true,
cancelButtonText: '{{trans('common.close')}}',
confirmButtonText: '{{trans('common.confirm')}}',
}).then((result) => {
if (result.value) {
$.ajax({
method: 'DELETE',
url: '{{route('admin.coupon.destroy', '')}}/' + id,
data: {_token: '{{csrf_token()}}'},
dataType: 'json',
success: function(ret) {
if (ret.status === 'success') {
swal.fire({title: ret.message, icon: 'success', timer: 1000, showConfirmButton: false}).then(() => window.location.reload());
} else {
swal.fire({title: ret.message, icon: 'error'}).then(() => window.location.reload());
}
},
});
}
});
}
}
});
}
@endcan
</script>
@endsection

View File

@@ -0,0 +1,234 @@
@extends('admin.layouts')
@section('css')
<link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
<link href="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.css" rel="stylesheet">
<style>
.text-fit {
width: fit-content;
width: -moz-fit-content;
}
</style>
@endsection
@section('content')
<div class="page-content container">
<div class="panel">
<div class="panel-heading">
<h1 class="panel-title">卡券信息</h1>
<div class="panel-actions">
<a href="{{route('admin.coupon.index')}}" class="btn btn-danger"> </a>
</div>
</div>
<div class="panel-body">
<div class="form-group row">
<label class="col-md-2 col-form-label" for="name">卡券名称</label>
<div class="col-md-10">
<input class="form-control text-fit" id="name" value="{{$coupon->name}}" disabled/>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="sn">使用券码</label>
<div class="col-md-10">
<input type="text" class="form-control text-fit" id="sn" value="{{$coupon->sn}}" disabled/>
</div>
</div>
@if($coupon->logo)
<div class="form-group row">
<span class="col-md-2 col-form-label">卡券图片</span>
<div class="col-md-10">
<img src="{{asset($coupon->logo)}}" class="h-100" alt="优惠码logo"/>
</div>
</div>
@endif
<div class="form-group row">
<span class="col-md-2 col-form-label">类型</span>
<div class="col-md-10 align-items-center">
<div class="radio-custom radio-primary radio-inline">
<input type="radio" id="voucher" checked/>
<label for="voucher">
{{ ['未知卡券','抵用券','折扣券','充值券'][$coupon->type] }}
</label>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 col-form-label" for="value">优惠额度</label>
<div class="col-md-10">
<p class="form-control text-fit">
@switch ($coupon->type)
@case(1)
抵用 <code>{{$coupon->value}}</code>
@break
@case(2)
<code>{{$coupon->value}}</code> %
@break
@case(3)
充值 <code>{{$coupon->value}}</code>
@break
@default
未知卡券
@endswitch
</p>
</div>
</div>
@isset($coupon->priority)
<div class="form-group row">
<span class="col-md-2 col-form-label"> </span>
<div class="col-md-10">
<span class="form-control text-fit"> {{$coupon->priority}} </span>
</div>
</div>
@endisset
@isset($coupon->usable_times)
<div class="form-group row">
<span class="col-md-2 col-form-label">剩余使用次数</span>
<div class="col-md-10">
<span class="form-control text-fit"><code>{{$coupon->usable_times}}</code> </span>
</div>
</div>
@endisset
@if(!empty($coupon->limit))
<hr>
@isset($coupon->limit['minimum'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="minimum">满减条件</label>
<div class="col-md-10">
<p class="form-control text-fit">当支付金额超过<strong> {{$coupon->limit['minimum']}}</strong> 时,才能使用本优惠劵</p>
</div>
</div>
@endisset
@isset($coupon->limit['used'])
<div class="form-group row">
<span class="col-md-2 col-form-label">个人限用</span>
<div class="col-md-10">
<p class="form-control text-fit">符合条件的用户可以使用本券 <strong>{{$coupon->limit['used']}}</strong></p>
</div>
</div>
@endisset
@isset($coupon->limit['users']['levels'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="levels">等级限定</label>
<div class="col-md-10">
<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="levels" multiple disabled>
@foreach($levels as $key => $level)
<option value="{{$key}}">{{$level}}</option>
@endforeach
</select>
<span class="text-help"> 以上用户等级,方可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['users']['groups'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="groups">分组限定</label>
<div class="col-md-10">
<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="groups" multiple disabled>
@foreach($userGroups as $key => $group)
<option value="{{$key}}">{{$group}}</option>
@endforeach
</select>
<span class="text-help"> 以上用户分组,方可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['users']['white'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="users_whitelist">专属用户</label>
<div class="col-md-6">
<input class="form-control" data-plugin="tokenfield" id="users_whitelist" value="{{ implode(',', $coupon->limit['users']['white']) }}"
disabled/>
<span class="text-help"> 以上用户均可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['users']['black'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="users_blacklist">禁用用户</label>
<div class="col-md-6">
<input class="form-control" data-plugin="tokenfield" id="users_blacklist" value="{{ implode(',', $coupon->limit['users']['black']) }}"
disabled/>
<span class="text-help"> 以上用户均不可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['services']['white'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="services_whitelist">许可商品</label>
<div class="col-md-6">
<input class="form-control" data-plugin="tokenfield" id="services_whitelist" value="{{ implode(',', $coupon->limit['services']['white']) }}"
disabled/>
<span class="text-help"> 以上商品方可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['services']['black'])
<div class="form-group row">
<label class="col-md-2 col-form-label" for="services_blacklist">禁用商品</label>
<div class="col-md-6">
<input class="form-control" data-plugin="tokenfield" id="services_blacklist" value="{{ implode(',', $coupon->limit['services']['black']) }}"
disabled/>
<span class="text-help"> 以上商品不可使用本券</span>
</div>
</div>
@endisset
@isset($coupon->limit['users']['newbie'])
<div class="form-group row">
<label for="newbie" class="col-md-2 col-form-label">新人专属</label>
<div class="col-md-10">
<ul class="list-unstyled">
<li class="list-group-item p-0">
<div class="checkbox-custom checkbox-primary">
<input type="checkbox" id="coupon" {{ isset($coupon->limit['users']['newbie']['coupon']) ? 'checked' : '' }} disabled/>
<label for="coupon">首次用任意券</label>
</div>
</li>
<li class="list-group-item p-0">
<div class="checkbox-custom checkbox-primary">
<input type="checkbox" id="order" {{ isset($coupon->limit['users']['newbie']['order']) ? 'checked' : '' }} disabled/>
<label for="order">首单</label>
</div>
</li>
@isset($coupon->limit['users']['newbie']['days'])
<li class="list-group-item p-0">
<span class="form-control text-fit"> 创号 <code>{{$coupon->limit['users']['newbie']['days']}}</code> </span>
</li>
@endisset
</ul>
</div>
</div>
@endisset
<hr>
@endif
<div class="form-group row">
<label class="col-md-2 col-form-label">有效期</label>
<div class="col-md-6 input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="icon wb-calendar" aria-hidden="true"></i></span>
</div>
<span class="form-control"> {{$coupon->start_time}} </span>
<div class="input-group-prepend">
<span class="input-group-text"></span>
</div>
<span class="form-control"> {{$coupon->end_time}} </span>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('javascript')
<script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
<script src="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
<script src="/assets/global/js/Plugin/bootstrap-tokenfield.js"></script>
<script>
$(document).ready(function() {
@isset($coupon->limit['users']['levels'])
$('#levels').selectpicker('val', @json($coupon->limit['users']['levels']));
@endisset
@isset($coupon->limit['users']['groups'])
$('#groups').selectpicker('val', @json($coupon->limit['users']['groups']));
@endisset
});
</script>
@endsection

View File

@@ -77,9 +77,9 @@
const goods_price = '{{$goods->price}}';
$.ajax({
method: 'POST',
url: '{{route('redeemCoupon')}}',
url: '{{route('redeemCoupon', $goods)}}',
dataType: 'json',
data: {_token: '{{csrf_token()}}', coupon_sn: coupon_sn, price: '{{$goods->price}}'},
data: {_token: '{{csrf_token()}}', coupon_sn: coupon_sn},
success: function(ret) {
$('.input-group-prepend').remove();
if (ret.status === 'success') {

View File

@@ -59,7 +59,7 @@ Route::prefix('admin')->name('admin.')->group(function () {
});
Route::resource('goods', 'ShopController')->except('show'); // 商品管理
Route::resource('coupon', 'CouponController')->except('show', 'edit', 'update'); // 优惠券
Route::resource('coupon', 'CouponController')->except('edit', 'update'); // 优惠券
Route::get('coupon/export', 'CouponController@exportCoupon')->name('coupon.export'); // 导出优惠券
Route::prefix('aff')->name('aff.')->group(function () {

View File

@@ -14,8 +14,8 @@ Route::get('invoices', 'UserController@invoices')->name('invoice'); // 订单列
Route::post('closePlan', 'UserController@closePlan')->name('cancelPlan'); // 激活预支付套餐
Route::get('invoice/{sn}', 'UserController@invoiceDetail')->name('invoiceInfo'); // 订单明细
Route::post('resetUserTraffic', 'UserController@resetUserTraffic')->name('resetTraffic'); // 重置用户流量
Route::post('buy/{good}/redeem', 'UserController@redeemCoupon')->name('redeemCoupon'); // 使用优惠券
Route::get('buy/{good}', 'UserController@buy')->name('buy'); // 购买商品
Route::post('redeemCoupon', 'UserController@redeemCoupon')->name('redeemCoupon'); // 使用优惠券
Route::get('invite', 'UserController@invite')->name('invite'); // 邀请码
Route::post('makeInvite', 'UserController@makeInvite')->name('createInvite'); // 生成邀请码
Route::match(['get', 'post'], 'profile', 'UserController@profile')->name('profile'); // 修改个人信息