Quellcode durchsuchen

Add 添加客户端API接口和商品分类管理

Bob vor 4 Jahren
Ursprung
Commit
d226d62cbf

+ 3 - 1
.env.example

@@ -48,4 +48,6 @@ MAILGUN_DOMAIN=
 MAILGUN_SECRET=
 
 REDIRECT_HTTPS=true
-BAIDU_APP_AK =
+BAIDU_APP_AK =
+
+JWT_SECRET=

+ 72 - 0
app/Http/Controllers/Admin/Config/CategoryController.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Http\Controllers\Admin\Config;
+
+use App\Http\Controllers\Controller;
+use App\Models\GoodsCategory;
+use App\Models\Level;
+use Exception;
+use Illuminate\Http\Request;
+use Log;
+use Response;
+use Validator;
+
+class CategoryController extends Controller
+{
+    // 添加等级
+    public function store(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'name' => 'required',
+        ]);
+
+        if ($validator->fails()) {
+            return Response::json(['status' => 'fail', 'message' => $validator->errors()->all()]);
+        }
+
+        if (GoodsCategory::create($validator->validated())) {
+            return Response::json(['status' => 'success', 'message' => '提交成功']);
+        }
+
+        return Response::json(['status' => 'fail', 'message' => '操作失败']);
+    }
+
+    // 编辑等级
+    public function update(Request $request, GoodsCategory $category)
+    {
+        $validator = Validator::make($request->all(), [
+            'name' => 'required',
+            'sort' => 'required|numeric',
+        ]);
+
+        if ($validator->fails()) {
+            return Response::json(['status' => 'fail', 'message' => $validator->errors()->all()]);
+        }
+        if ($category->update($validator->validated())) {
+            return Response::json(['status' => 'success', 'message' => '操作成功']);
+        }
+
+        return Response::json(['status' => 'fail', 'message' => '操作失败']);
+    }
+
+    // 删除等级
+    public function destroy(GoodsCategory $category)
+    {
+        // 校验该等级下是否存在关联账号
+        if ($category->goods()->exists()) {
+            return Response::json(['status' => 'fail', 'message' => '该分类下存在关联账号,请先取消关联']);
+        }
+
+        try {
+            if ($category->delete()) {
+                return Response::json(['status' => 'success', 'message' => '删除成功']);
+            }
+        } catch (Exception $e) {
+            Log::error('删除时报错:'.$e->getMessage());
+
+            return Response::json(['status' => 'fail', 'message' => '删除失败:'.$e->getMessage()]);
+        }
+
+        return Response::json(['status' => 'fail', 'message' => '删除失败']);
+    }
+}

+ 4 - 1
app/Http/Controllers/Admin/ShopController.php

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
 use App\Http\Requests\Admin\ShopStoreRequest;
 use App\Http\Requests\Admin\ShopUpdateRequest;
 use App\Models\Goods;
+use App\Models\GoodsCategory;
 use App\Models\Level;
 use Arr;
 use Exception;
@@ -37,7 +38,7 @@ class ShopController extends Controller
     // 添加商品页面
     public function create()
     {
-        return view('admin.shop.info', ['levels' => Level::orderBy('level')->get()]);
+        return view('admin.shop.info', ['levels' => Level::orderBy('level')->get(), 'categories' => GoodsCategory::all()]);
     }
 
     // 添加商品
@@ -92,6 +93,7 @@ class ShopController extends Controller
         return view('admin.shop.info', [
             'good'   => $good,
             'levels' => Level::orderBy('level')->get(),
+            'categories' => GoodsCategory::all(),
         ]);
     }
 
@@ -109,6 +111,7 @@ class ShopController extends Controller
                 return $path;
             }
         }
+
         try {
             $data['is_hot'] = array_key_exists('is_hot', $data) ? 1 : 0;
             $data['status'] = array_key_exists('status', $data) ? 1 : 0;

+ 2 - 0
app/Http/Controllers/AdminController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers;
 
 use App\Models\Country;
+use App\Models\GoodsCategory;
 use App\Models\Invite;
 use App\Models\Label;
 use App\Models\Level;
@@ -118,6 +119,7 @@ class AdminController extends Controller
         return view('admin.config.config', [
             'methods' => SsConfig::type(1)->get(),
             'protocols' => SsConfig::type(2)->get(),
+            'categories' => GoodsCategory::all(),
             'obfsList' => SsConfig::type(3)->get(),
             'countries' => Country::all(),
             'levels' => Level::all(),

+ 249 - 24
app/Http/Controllers/Api/Client/V1Controller.php

@@ -2,17 +2,30 @@
 
 namespace App\Http\Controllers\Api\Client;
 
+use App\Components\Helpers;
 use App\Http\Controllers\Controller;
+use App\Http\Controllers\PaymentController;
+use App\Models\Coupon;
 use App\Models\Goods;
+use App\Models\GoodsCategory;
+use App\Models\Order;
+use App\Models\Payback;
+use App\Models\Payment;
+use App\Models\ReferralLog;
 use App\Models\User;
+use Hashids\Hashids;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
 use Validator;
 
 class V1Controller extends Controller
 {
+    private static $method;
+
     public function __construct()
     {
-        $this->middleware('auth:api', ['except' => ['login', 'register', 'shop']]);
+        $this->middleware('auth:api', ['except' => ['login', 'register', 'shop', 'config', 'getConfig']]);
         auth()->shouldUse('api');
     }
 
@@ -27,7 +40,7 @@ class V1Controller extends Controller
             return response()->json(['ret' => 0, 'msg' => $validator->errors()->all()], 422);
         }
 
-        if ($token = auth()->attempt($validator->validated())) {
+        if ($token = auth('api')->attempt($validator->validated())) {
             return $this->createNewToken($token);
         }
 
@@ -37,18 +50,20 @@ class V1Controller extends Controller
     protected function createNewToken($token)
     {
         return response()->json([
-            'ret'          => 1,
-            'access_token' => $token,
-            'token_type'   => 'bearer',
-            'expires_in'   => auth()->factory()->getTTL() * 60,
-            'user'         => auth()->user()->profile(),
+            'ret' => 1,
+            'data' => [
+                'access_token' => $token,
+                'token_type' => 'bearer',
+                'expires_in' => auth('api')->factory()->getTTL() * 60,
+                'user' => auth('api')->user()->profile(),
+            ],
         ]);
     }
 
     public function register(Request $request)
     {
         $validator = Validator::make($request->all(), [
-            'name'     => 'required|string|between:2,100',
+            'name' => 'required|string|between:2,100',
             'username' => 'required|'.(sysConfig('username_type') ?? 'email').'|max:100|unique:user,username',
             'password' => 'required|string|confirmed|min:6',
         ]);
@@ -67,46 +82,256 @@ class V1Controller extends Controller
 
     public function logout()
     {
-        auth()->logout();
+        auth('api')->logout();
 
         return response()->json(['ret' => 1]);
     }
 
     public function refresh()
     {
-        return $this->createNewToken(auth()->refresh());
+        return $this->createNewToken(auth('api')->refresh());
     }
 
     public function userProfile()
     {
-        return response()->json(auth()->user()->profile());
+        $user = auth('api')->user();
+        $userInfo = $user->profile();
+        $userInfo['subUrl'] = $user->subUrl();
+        $totalTransfer = $user->transfer_enable;
+        $usedTransfer = $user->used_traffic;
+        $unusedTraffic = $totalTransfer - $usedTransfer > 0 ? $totalTransfer - $usedTransfer : 0;
+        $userInfo['unusedTraffic'] = flowAutoShow($unusedTraffic);
+
+        return response()->json(['ret' => 1, 'data' => $userInfo]);
     }
 
     public function nodeList(int $id = null)
     {
-        $user = auth()->user();
+        $user = auth('api')->user();
         $nodes = $user->nodes()->get();
-        if (isset($id)) {
-            $node = $nodes->find($id);
 
-            if (empty($node)) {
-                return response()->json([], 204);
+        return response()->json(['ret' => 1, 'data' => $nodes]);
+    }
+
+    public function shop()
+    {
+        $shops = [
+            'keys' => [],
+            'data' => [],
+        ];
+        $shop_plan = GoodsCategory::query()->where('status', 1)->get();
+        foreach ($shop_plan as $item) {
+            array_push($shops['keys'], $item['name']);
+            $shops['data'][$item['name']] = $item->goods()->get()->append('traffic_label')->toArray();
+        }
+
+        return response()->json(['ret' => 1, 'data' => $shops]);
+    }
+
+    public function getConfig()
+    {
+        $config = config('bobclient');
+        $config['website_name'] = sysConfig('website_name');
+        $config['website_url'] = sysConfig('website_url');
+        $config['payment'] = [
+            'alipay' => sysConfig('is_AliPay'),
+            'wechat' => sysConfig('is_WeChatPay'),
+        ];
+
+        return response()->json(['ret' => 1, 'data' => $config]);
+    }
+
+    // 创建支付订单
+    public function purchase(Request $request)
+    {
+        $goods_id = $request->input('goods_id');
+        $coupon_sn = $request->input('coupon_sn');
+        self::$method = $request->input('method');
+        $credit = $request->input('amount');
+        $pay_type = $request->input('pay_type');
+        $amount = 0;
+
+        // 充值余额
+        if ($credit) {
+            if (! is_numeric($credit) || $credit <= 0) {
+                return response()->json(['ret' => 0, 'msg' => trans('user.payment.error')]);
+            }
+            $amount = $credit;
+        // 购买服务
+        } elseif ($goods_id && self::$method) {
+            $goods = Goods::find($goods_id);
+            if (! $goods || ! $goods->status) {
+                return response()->json(['ret' => 0, 'msg' => '订单创建失败:商品已下架']);
+            }
+            $amount = $goods->price;
+
+            // 是否有生效的套餐
+            $activePlan = Order::userActivePlan()->doesntExist();
+
+            // 无生效套餐,禁止购买加油包
+            if ($goods->type === 1 && $activePlan) {
+                return response()->json(['ret' => 0, 'msg' => '购买加油包前,请先购买套餐']);
+            }
+
+            // 单个商品限购
+            if ($goods->limit_num) {
+                $count = Order::uid()->where('status', '>=', 0)->whereGoodsId($goods_id)->count();
+                if ($count >= $goods->limit_num) {
+                    return response()->json(['ret' => 0, 'msg' => '此商品限购'.$goods->limit_num.'次,您已购买'.$count.'次']);
+                }
+            }
+
+            // 使用优惠券
+            if ($coupon_sn) {
+                $coupon = Coupon::whereStatus(0)->whereIn('type', [1, 2])->whereSn($coupon_sn)->first();
+                if (! $coupon) {
+                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:优惠券不存在']);
+                }
+
+                // 计算实际应支付总价
+                $amount = $coupon->type === 2 ? $goods->price * $coupon->value / 100 : $goods->price - $coupon->value;
+                $amount = $amount > 0 ? round($amount, 2) : 0; // 四舍五入保留2位小数,避免无法正常创建订单
+            }
+
+            //非余额付款下,检查在线支付是否开启
+            if (self::$method !== 'credit') {
+                // 判断是否开启在线支付
+                if (! sysConfig('is_onlinePay')) {
+                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:系统并未开启在线支付功能']);
+                }
+
+                // 判断是否存在同个商品的未支付订单
+                if (Order::uid()->whereStatus(0)->exists()) {
+                    return response()->json(['ret' => 0, 'msg' => '订单创建失败:尚有未支付的订单,请先去支付']);
+                }
+            } elseif (Auth::getUser()->credit < $amount) { // 验证账号余额是否充足
+                return response()->json(['ret' => 0, 'msg' => '您的余额不足,请先充值']);
+            }
+
+            // 价格异常判断
+            if ($amount < 0) {
+                return response()->json(['ret' => 0, 'msg' => '订单创建失败:订单总价异常']);
+            }
+
+            if ($amount === 0 && self::$method !== 'credit') {
+                return response()->json(['ret' => 0, 'msg' => '订单创建失败:订单总价为0,无需使用在线支付']);
+            }
+        }
+
+        // 生成订单
+        try {
+            $newOrder = Order::create([
+                'sn' => date('ymdHis').random_int(100000, 999999),
+                'user_id' => auth()->id(),
+                'goods_id' => $credit ? null : $goods_id,
+                'coupon_id' => $coupon->id ?? null,
+                'origin_amount' => $credit ?: $goods->price ?? 0,
+                'amount' => $amount,
+                'pay_type' => $pay_type,
+                'pay_way' => self::$method,
+            ]);
+
+            // 使用优惠券,减少可使用次数
+            if (! empty($coupon)) {
+                if ($coupon->usable_times > 0) {
+                    $coupon->decrement('usable_times', 1);
+                }
+
+                Helpers::addCouponLog('订单支付使用', $coupon->id, $goods_id, $newOrder->id);
             }
 
-            return response()->json($node->config($user));
+            $request->merge(['id' => $newOrder->id, 'type' => $pay_type, 'amount' => $amount]);
+            PaymentController::$method = self::$method;
+            // 生成支付单
+            $data = PaymentController::getClient()->purchase($request);
+            $data = $data->getData(true);
+            $data['order_id'] = $newOrder->id;
+
+            return response()->json($data);
+        } catch (Exception $e) {
+            Log::error('订单生成错误:'.$e->getMessage());
         }
-        $servers = [];
-        foreach ($nodes as $node) {
-            $servers[] = $node->config($user);
+
+        return response()->json(['ret' => 0, 'msg' => '订单创建失败']);
+    }
+
+    /**
+     * @param Request $request
+     * @return JsonResponse
+     */
+    public static function getStatus(Request $request): JsonResponse
+    {
+        $order_id = $request->input('order_id');
+        $payment = Order::query()->find($order_id)->payment;
+        if ($payment) {
+            if ($payment->status === 1) {
+                return response()->json(['ret' => 1, 'msg' => '支付成功']);
+            }
+
+            if ($payment->status === -1) {
+                return response()->json(['ret' => 0, 'msg' => '订单超时未支付,已自动关闭']);
+            }
+
+            return response()->json(['ret' => 0, 'msg' => '等待支付']);
         }
 
-        return response()->json($servers);
+        return response()->json(['ret' => 0, 'msg' => '未知订单']);
     }
 
-    public function shop()
+    public function gift(Request $request)
     {
-        $shop = Goods::whereStatus(1)->where('type', '<=', '2')->orderByDesc('type')->orderByDesc('sort')->get();
+        $user = $request->user('api');
+        $referral_traffic = flowAutoShow(sysConfig('referral_traffic') * MB);
+        $referral_percent = sysConfig('referral_percent');
+        // 邀请码
+        $code = $user->invites()->whereStatus(1)->value('code');
+
+        $data['invite_gift'] = trans('user.invite.promotion', [
+            'traffic' => $referral_traffic,
+            'referral_percent' => $referral_percent * 100,
+        ]);
+        $affSalt = sysConfig('aff_salt');
+        if (isset($affSalt)) {
+            $aff_link = route('register', ['aff' => (new Hashids($affSalt, 8))->encode($user->id)]);
+        } else {
+            $aff_link = route('register', ['aff' => $user->id]);
+        }
+        $data['invite_url'] = $aff_link;
+        $data['invite_text'] = $aff_link.'&(复制整段文字到浏览器打开即可访问),找梯子最重要的就是稳定,这个已经上线三年了,一直稳定没有被封过,赶紧下载备用吧!安装后打开填写我的邀请码【'.$code.'】,你还能多得3天会员.';
+        // 累计数据
+        $data['back_sum'] = ReferralLog::query()->where('inviter_id', $user->id)->sum('commission') / 100;
+        $data['user_sum'] = $user->invitees()->count();
+        $data['list'] = $user->invitees()->selectRaw('username, UNIX_TIMESTAMP(created_at) as created_at')->limit(10)->get();
+
+        return response()->json(['ret' => 1, 'data' => $data]);
+    }
+
+    public function checkIn(Request $request): JsonResponse
+    {
+        $user = $request->user();
+        // 系统开启登录加积分功能才可以签到
+        if (! sysConfig('is_checkin')) {
+            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.disable')]);
+        }
+
+        // 已签到过,验证是否有效
+        if (Cache::has('userCheckIn_'.$user->id)) {
+            return response()->json(['ret' => 0, 'title' => trans('common.success'), 'msg' => trans('user.home.attendance.done')]);
+        }
+
+        $traffic = random_int((int) sysConfig('min_rand_traffic'), (int) sysConfig('max_rand_traffic')) * MB;
+
+        if (! $user->incrementData($traffic)) {
+            return response()->json(['ret' => 0, 'title' => trans('common.failed'), 'msg' => trans('user.home.attendance.failed')]);
+        }
+
+        // 写入用户流量变动记录
+        Helpers::addUserTrafficModifyLog($user->id, null, $user->transfer_enable, $user->transfer_enable + $traffic, trans('user.home.attendance.attribute'));
+
+        // 多久后可以再签到
+        $ttl = sysConfig('traffic_limit_time') ? sysConfig('traffic_limit_time') * Minute : Day;
+        Cache::put('userCheckIn_'.$user->id, '1', $ttl);
 
-        return response()->json($shop);
+        return response()->json(['ret' => 1, 'msg' => trans('user.home.attendance.success', ['data' => flowAutoShow($traffic)])]);
     }
 }

+ 1 - 1
app/Http/Controllers/PaymentController.php

@@ -26,7 +26,7 @@ use Response;
 
 class PaymentController extends Controller
 {
-    private static $method;
+    public static $method;
 
     public static function notify(Request $request): void
     {

+ 17 - 0
app/Models/GoodsCategory.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+class GoodsCategory extends Model
+{
+    protected $table = 'goods_category';
+    protected $guarded = [];
+
+    public function goods(): HasMany
+    {
+        return $this->hasMany(Goods::class, 'category_id');
+    }
+}

+ 53 - 0
config/bobclient.php

@@ -0,0 +1,53 @@
+<?php
+
+// ┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐ \\
+// │ BobVPN                                                                                                │ \\
+// ├───────────────────────────────────────────────────────────────────────────────────────────────────────┤ \\
+// │ Copyright © 2021 (https://t.me/Bobs9)                                                                 │ \\
+// └───────────────────────────────────────────────────────────────────────────────────────────────────────┘ \\
+
+return [
+    // 登录页面配置
+    'login' => [
+        'telegram_url' => 'https://t.me/Bobs9',  // 留空的话则不展示telegram群
+        'qq_url' => 'https://t.me/Bobs9',  // 留空的话则不展示QQ群
+        'background_img' => 'https://shige.group/such/pic.php/forum/pic/item/00e93901213fb80e3d28759b21d12f2eb8389484/mlike.jpg', // 背景图片地址,图片宽高不超过 860px * 544px 就行 (留空为默认的背景图)
+        'text' => '一键开启<br>极速上网体验',
+        'text_color' => 'rgba(255, 255, 255, 0.8);',    // 文字和按钮颜色   默认颜色 rgba(255, 255, 255, 0.8);
+        'button_color' => '#8077f1',    // 文字和按钮颜色 默认颜色:#8077f1(v2版本配置)
+    ],
+
+    // PC端消息中心图片和跳转链接
+    'message' => [
+        'background_img' => 'https://malus.s3cdn.net/uploads/malus_user-guide.jpg', // 背景图片地址
+        'url' => 'https://www.goole.com',    // 跳转链接
+    ],
+
+    // Crisp在线客服
+    'crisp_enable' => false,  // 是否开启
+    'crisp_id' => '2c3c28c2-9265-45ea-8e85-0xxxxx',       // Crisp 的网站ID
+
+    // 弹窗公告
+    'notice' => [
+        'is_start' => true, // 是否开启弹窗公告
+        'title' => '最新公告', // 标题
+        'content' => '<strong>这是最新 <i>公告</i> 内容</strong>', // 公告内容,可以为html格式,也可以纯文本
+    ],
+
+    // PC端菜单栏显示控制
+    'menu' => [
+        'shop' => true,          // 会员
+        'user' => true,          // 我的
+        'gift' => true,          // 邀请
+    ],
+
+    // 检查用户计算机时间
+    'check_time' => [
+        'is_check' => true,  // 是否开启检查
+        'differ_time' => 90,    // 相差多少秒提示
+        'warning_text' => '请校准系统时间为北京时间,否则会导致无法上网!', // 提示内容
+    ],
+
+    // 个人中心头像
+    'user_avatar' => 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=4109802972,297162689&fm=11&gp=0.jpg',
+];

+ 44 - 0
database/migrations/2021_07_24_214642_create_goods_category_table.php

@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Spatie\Permission\Models\Permission;
+
+class CreateGoodsCategoryTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('goods_category', function (Blueprint $table) {
+            $table->id();
+            $table->string('name')->default('')->comment('分类名称');
+            $table->tinyInteger('status')->default('1')->comment('状态 0:隐藏 1:显示');
+            $table->integer('sort')->default('0')->comment('排序');
+            $table->timestamps();
+        });
+        Schema::table('goods', function (Blueprint $table) {
+            $table->integer('category_id')->default(1)->nullable()->comment('分类ID');
+        });
+        \App\Models\GoodsCategory::query()->create(['name' => '黄金套餐']);
+        \App\Models\GoodsCategory::query()->create(['name' => '白金套餐']);
+        \App\Models\GoodsCategory::query()->create(['name' => '钻石套餐']);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('goods_category');
+        Schema::table('goods', function (Blueprint $table) {
+            $table->dropColumn(['category_id']);
+        });
+    }
+}

+ 170 - 0
resources/views/admin/config/config.blade.php

@@ -30,6 +30,9 @@
                         <li class="nav-item" role="presentation">
                             <a class="nav-link" data-toggle="tab" href="#label" aria-controls="label" role="tab">标签</a>
                         </li>
+                        <li class="nav-item" role="presentation">
+                            <a class="nav-link" data-toggle="tab" href="#category" aria-controls="category" role="tab">商品分类</a>
+                        </li>
                     </ul>
                     <div class="tab-content py-15">
                         <div class="tab-pane active" id="method" role="tabpanel">
@@ -168,6 +171,40 @@
                                 </tbody>
                             </table>
                         </div>
+                        <div class="tab-pane" id="category" role="tabpanel">
+                            <button class="btn btn-primary float-right mb-10" data-toggle="modal" data-target="#add_category_modal">
+                                新增<i class="icon wb-plus"></i>
+                            </button>
+                            <table class="text-md-center" data-toggle="table" data-height="700" data-virtual-scroll="true" data-mobile-responsive="true">
+                                <thead class="thead-default">
+                                <tr>
+                                    <th> 名称</th>
+                                    <th> 排序</th>
+                                    <th> {{trans('common.action')}}</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach($categories as $category)
+                                    <tr>
+                                        <td>
+                                            <input type="text" class="form-control" name="name" id="category_name_{{$category->id}}" value="{{$category->name}}"/>
+                                        </td>
+                                        <td>
+                                            <input type="text" class="form-control" name="sort" id="category_sort_{{$category->id}}" value="{{$category->sort}}"/>
+                                        </td>
+                                        <td>
+                                            <div class="btn-group">
+                                                <button type="button" class="btn btn-primary" onclick="updateCategory('{{$category->id}}')">
+                                                    <i class="icon wb-edit" aria-hidden="true"></i></button>
+                                                <button type="button" class="btn btn-danger" onclick="delCategory('{{$category->id}}','{{$category->name}}')">
+                                                    <i class="icon wb-trash"></i></button>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
                         <div class="tab-pane" id="country" role="tabpanel">
                             <button class="btn btn-primary float-right mb-10" data-toggle="modal" data-target="#add_country_modal">
                                 新增<i class="icon wb-plus"></i>
@@ -310,6 +347,34 @@
         </div>
     </div>
 
+    <div class="modal fade" id="add_category_modal" aria-hidden="true" role="dialog" tabindex="-1">
+        <div class="modal-dialog modal-simple modal-center">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                        <span aria-hidden="true">×</span>
+                    </button>
+                    <h4 class="modal-title">新增分类</h4>
+                </div>
+                <form action="#" method="post" class="modal-body">
+                    <div class="alert alert-danger" style="display: none;" id="category_msg"></div>
+                    <div class="row">
+                        <div class="col-md-6 form-group">
+                            <input type="text" class="form-control" name="name" id="add_category_name" placeholder="分类名称">
+                        </div>
+                        <div class="col-md-6 form-group">
+                            <input type="text" class="form-control" name="sort" id="add_category_sort" placeholder="分类排序">
+                        </div>
+                    </div>
+                </form>
+                <div class="modal-footer">
+                    <button data-dismiss="modal" class="btn btn-danger">关 闭</button>
+                    <button class="btn btn-primary" onclick="addCategory()">提 交</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
     <div class="modal fade" id="add_country_modal" aria-hidden="true" role="dialog" tabindex="-1">
         <div class="modal-dialog modal-simple modal-center">
             <div class="modal-content">
@@ -476,6 +541,111 @@
         swal.fire({title: '您没有权限修改参数!', icon: 'error', timer: 1500, showConfirmButton: false});
         @endcan
 
+        @can('admin.config.category.store')
+        // 添加分类
+        function addCategory() {
+            const name = $('#add_category_name').val();
+            const sort = $('#add_category_sort').val();
+
+            if (name.trim() === '') {
+                $('#category_msg').show().html('分类名称不能为空');
+                $('#category_name').focus();
+                return false;
+            }
+
+            if (sort.trim() === '') {
+                $('#category_msg').show().html('分类排序不能为空');
+                $('#category_sort').focus();
+                return false;
+            }
+
+            $.ajax({
+                url: '{{route('admin.config.category.store')}}',
+                method: 'POST',
+                data: {_token: '{{csrf_token()}}', name: name, sort: sort},
+                beforeSend: function() {
+                    $('#category_msg').show().html('正在添加');
+                },
+                success: function(ret) {
+                    if (ret.status === 'fail') {
+                        $('#category_msg').show().html(ret.message);
+                        return false;
+                    }
+                    $('#add_category_modal').modal('hide');
+                    window.location.reload();
+                },
+                error: function() {
+                    $('#category_msg').show().html('请求错误,请重试');
+                },
+                complete: function() {
+                    swal.fire({title: '添加成功', icon: 'success', timer: 1000, showConfirmButton: false}).then(() => window.location.reload());
+                },
+            });
+        }
+
+        @else
+        swal.fire({title: '您没有权限修改参数!', icon: 'error', timer: 1500, showConfirmButton: false});
+        @endcan
+
+        @can('admin.config.category.update')
+        // 更新分类
+        function updateCategory(id) {
+            $.ajax({
+                method: 'PUT',
+                url: '{{route('admin.config.category.update', '')}}/' + id,
+                data: {
+                    _token: '{{csrf_token()}}',
+                    name: $('#category_name_' + id).val(),
+                    sort: $('#category_sort_' + id).val(),
+                },
+                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());
+                    }
+                },
+            });
+        }
+
+        @else
+        swal.fire({title: '您没有权限修改参数!', icon: 'error', timer: 1500, showConfirmButton: false});
+        @endcan
+
+        @can('admin.config.category.destroy')
+        // 删除分类
+        function delCategory(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.config.category.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());
+                            }
+                        },
+                    });
+                }
+            });
+        }
+
+        @else
+        swal.fire({title: '您没有权限修改参数!', icon: 'error', timer: 1500, showConfirmButton: false});
+        @endcan
+
         @can('admin.config.country.store')
         // 添加国家/地区
         function addCountry() {

+ 11 - 0
resources/views/admin/shop/info.blade.php

@@ -56,6 +56,16 @@
                                     <span class="input-group-text">元</span>
                                 </div>
                             </div>
+                            <div class="form-group row">
+                                <label for="level" class="col-md-2 col-form-label">分类</label>
+                                <div class="col-md-4">
+                                    <select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="form-control" name="category_id" id="category_id">
+                                        @foreach ($categories as $category)
+                                        <option value="{{$category->id}}">{{$category->name}}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
+                            </div>
                             <div class="form-group row">
                                 <label for="level" class="col-md-2 col-form-label">等级</label>
                                 <div class="col-md-4">
@@ -221,6 +231,7 @@
             $('#color').asColorPicker('val', '{{$good->color}}');
             $('#description').val(@json($good->description));
             $('#info').val(@json($good->info));
+            $('#category_id').selectpicker('val', '{{$good->category_id}}');
             const trafficUnit = $('#traffic_unit');
             const traffic = $('#traffic');
             @if($good->traffic >= 1073741824)

+ 1 - 0
routes/admin.php

@@ -94,6 +94,7 @@ Route::prefix('admin')->name('admin.')->group(function () {
             Route::resource('label', 'LabelController')->only('store', 'update', 'destroy'); // 标签配置
             Route::resource('level', 'LevelController')->only('store', 'update', 'destroy'); // 等级配置
             Route::resource('ss', 'SsConfigController')->only('store', 'update', 'destroy'); // ss配置
+            Route::resource('category', 'CategoryController')->only('store', 'update', 'destroy'); // 商品分类配置
         });
 
         Route::resource('permission', 'PermissionController')->except('show');

+ 15 - 8
routes/api.php

@@ -49,14 +49,21 @@ Route::group(['namespace' => 'Api\WebApi', 'middleware' => 'webApi'], function (
 });
 
 // 客户端API
-Route::group(['namespace' => 'Api\Client', 'middleware' => 'api', 'prefix' => 'client/v1'], function () {
+Route::group(['namespace' => 'Api\Client', 'prefix' => 'client/v1'], function () {
+    Route::get('config', 'V1Controller@getConfig'); // 获取配置
     Route::post('login', 'V1Controller@login'); // 登录
-    Route::get('logout', 'V1Controller@logout'); // 退出
-    Route::get('refresh', 'V1Controller@refresh'); // 刷新令牌
-    Route::get('profile', 'V1Controller@userProfile'); // 获取账户信息
-    Route::get('nodes', 'V1Controller@nodeList'); // 获取账户全部节点
-    Route::get('node/{id}', 'V1Controller@nodeList'); // 获取账户个别节点
-
     Route::post('register', 'V1Controller@register'); // 注册
-    Route::get('shop', 'V1Controller@shop'); // 获取商品信息
+
+    Route::group(['middleware' => 'auth:api'], function () {
+        Route::get('logout', 'V1Controller@logout'); // 退出
+        Route::get('refresh', 'V1Controller@refresh'); // 刷新令牌
+        Route::get('profile', 'V1Controller@userProfile'); // 获取账户信息
+        Route::get('nodes', 'V1Controller@nodeList'); // 获取账户全部节点
+        Route::get('node/{id}', 'V1Controller@nodeList'); // 获取账户个别节点
+        Route::get('shop', 'V1Controller@shop'); // 获取商品信息
+        Route::get('gift', 'V1Controller@gift'); // 获取邀请信息
+        Route::post('checkIn', 'V1Controller@checkIn'); // 签到
+        Route::post('payment/purchase', 'V1Controller@purchase'); // 获取商品信息
+        Route::get('payment/getStatus', 'V1Controller@getStatus'); // 获取商品信息
+    });
 });