Selaa lähdekoodia

1.节点订阅地址(防投毒)
2.优化订阅地址(缩短长度)
3.标签管理
4.用户、节点设置标签

zhangjiangbin 8 vuotta sitten
vanhempi
sitoutus
7b556dcce7

+ 214 - 66
app/Http/Controllers/AdminController.php

@@ -3,10 +3,10 @@
 namespace App\Http\Controllers;
 
 use App\Http\Models\Article;
-use App\Http\Models\ArticleLog;
 use App\Http\Models\Config;
 use App\Http\Models\Country;
 use App\Http\Models\Invite;
+use App\Http\Models\Label;
 use App\Http\Models\Level;
 use App\Http\Models\Order;
 use App\Http\Models\OrderGoods;
@@ -17,12 +17,14 @@ use App\Http\Models\SsGroup;
 use App\Http\Models\SsGroupNode;
 use App\Http\Models\SsNode;
 use App\Http\Models\SsNodeInfo;
+use App\Http\Models\SsNodeLabel;
 use App\Http\Models\SsNodeOnlineLog;
 use App\Http\Models\SsNodeTrafficDaily;
 use App\Http\Models\SsNodeTrafficHourly;
 use App\Http\Models\User;
 use App\Http\Models\UserBalanceLog;
 use App\Http\Models\UserBanLog;
+use App\Http\Models\UserLabel;
 use App\Http\Models\UserSubscribe;
 use App\Http\Models\UserSubscribeLog;
 use App\Http\Models\UserTrafficDaily;
@@ -174,6 +176,17 @@ class AdminController extends Controller
             $user->save();
 
             if ($user->id) {
+                // 生成用户标签
+                $labels = $request->get('labels');
+                if (!empty($labels)) {
+                    foreach ($labels as $label) {
+                        $userLabel = new UserLabel();
+                        $userLabel->user_id = $user->id;
+                        $userLabel->label_id = $label;
+                        $userLabel->save();
+                    }
+                }
+
                 return Response::json(['status' => 'success', 'data' => '', 'message' => '添加成功']);
             } else {
                 return Response::json(['status' => 'fail', 'data' => '', 'message' => '添加失败']);
@@ -183,11 +196,11 @@ class AdminController extends Controller
             $last_user = User::query()->orderBy('id', 'desc')->first();
             $view['last_port'] = self::$config['is_rand_port'] ? $this->getRandPort() : $last_user->port + 1;
 
-            // 加密方式、协议、混淆、等级
             $view['method_list'] = $this->methodList();
             $view['protocol_list'] = $this->protocolList();
             $view['obfs_list'] = $this->obfsList();
             $view['level_list'] = $this->levelList();
+            $view['label_list'] = Label::query()->orderBy('sort', 'desc')->orderBy('id', 'asc')->get();
 
             return Response::view('admin/addUser', $view);
         }
@@ -252,62 +265,88 @@ class AdminController extends Controller
             $usage = $request->get('usage');
             $pay_way = $request->get('pay_way');
             $status = $request->get('status');
+            $labels = $request->get('labels');
             $enable_time = $request->get('enable_time');
             $expire_time = $request->get('expire_time');
             $remark = $request->get('remark');
             $level = $request->get('level');
             $is_admin = $request->get('is_admin');
 
-            $data = [
-                'username'             => $username,
-                'port'                 => $port,
-                'passwd'               => $passwd,
-                'transfer_enable'      => $this->toGB($transfer_enable),
-                'enable'               => $status < 0 ? 0 : $enable, // 如果禁止登陆则同时禁用SSR
-                'method'               => $method,
-                'protocol'             => $protocol,
-                'protocol_param'       => $protocol_param,
-                'obfs'                 => $obfs,
-                'obfs_param'           => $obfs_param,
-                'speed_limit_per_con'  => $speed_limit_per_con,
-                'speed_limit_per_user' => $speed_limit_per_user,
-                'gender'               => $gender,
-                'wechat'               => $wechat,
-                'qq'                   => $qq,
-                'usage'                => $usage,
-                'pay_way'              => $pay_way,
-                'status'               => $status,
-                'enable_time'          => empty($enable_time) ? date('Y-m-d') : $enable_time,
-                'expire_time'          => empty($expire_time) ? date('Y-m-d', strtotime("+365 days")) : $expire_time,
-                'remark'               => $remark,
-                'level'                => $level,
-                'is_admin'             => $is_admin
-            ];
+            DB::beginTransaction();
+            try {
+                $data = [
+                    'username'             => $username,
+                    'port'                 => $port,
+                    'passwd'               => $passwd,
+                    'transfer_enable'      => $this->toGB($transfer_enable),
+                    'enable'               => $status < 0 ? 0 : $enable, // 如果禁止登陆则同时禁用SSR
+                    'method'               => $method,
+                    'protocol'             => $protocol,
+                    'protocol_param'       => $protocol_param,
+                    'obfs'                 => $obfs,
+                    'obfs_param'           => $obfs_param,
+                    'speed_limit_per_con'  => $speed_limit_per_con,
+                    'speed_limit_per_user' => $speed_limit_per_user,
+                    'gender'               => $gender,
+                    'wechat'               => $wechat,
+                    'qq'                   => $qq,
+                    'usage'                => $usage,
+                    'pay_way'              => $pay_way,
+                    'status'               => $status,
+                    'enable_time'          => empty($enable_time) ? date('Y-m-d') : $enable_time,
+                    'expire_time'          => empty($expire_time) ? date('Y-m-d', strtotime("+365 days")) : $expire_time,
+                    'remark'               => $remark,
+                    'level'                => $level,
+                    'is_admin'             => $is_admin
+                ];
 
-            if (!empty($password)) {
-                $data['password'] = md5($password);
-            }
+                if (!empty($password)) {
+                    $data['password'] = md5($password);
+                }
+
+                User::query()->where('id', $id)->update($data);
+
+                // 生成用户标签
+                if (!empty($labels)) {
+                    // 先删除所有该用户的标签
+                    UserLabel::query()->where('user_id', $id)->delete();
+
+                    foreach ($labels as $label) {
+                        $userLabel = new UserLabel();
+                        $userLabel->user_id = $id;
+                        $userLabel->label_id = $label;
+                        $userLabel->save();
+                    }
+                }
+
+                DB::commit();
 
-            $ret = User::query()->where('id', $id)->update($data);
-            if ($ret) {
                 return Response::json(['status' => 'success', 'data' => '', 'message' => '编辑成功']);
-            } else {
+            } catch (\Exception $e) {
+                DB::rollBack();
+                Log::error('编辑用户信息异常:' . $e->getMessage());
+
                 return Response::json(['status' => 'fail', 'data' => '', 'message' => '编辑失败']);
             }
         } else {
-            $user = User::query()->where('id', $id)->first();
+            $user = User::query()->with(['label'])->where('id', $id)->first();
             if ($user) {
                 $user->transfer_enable = $this->flowToGB($user->transfer_enable);
                 $user->balance = $user->balance / 100;
+
+                $label = [];
+                foreach ($user->label as $vo) {
+                    $label[] = $vo->label_id;
+                }
+                $user->labels = $label;
             }
 
             $view['user'] = $user;
-
-            // 加密方式、协议、混淆、等级
             $view['method_list'] = $this->methodList();
             $view['protocol_list'] = $this->protocolList();
             $view['obfs_list'] = $this->obfsList();
             $view['level_list'] = $this->levelList();
+            $view['label_list'] = Label::query()->orderBy('sort', 'desc')->orderBy('id', 'asc')->get();
 
             return Response::view('admin/editUser', $view);
         }
@@ -393,6 +432,17 @@ class AdminController extends Controller
                 $ssGroupNode->save();
             }
 
+            // 生成节点标签
+            $labels = $request->get('labels');
+            if ($ssNode->id && !empty($labels)) {
+                foreach ($labels as $label) {
+                    $ssNodeLabel = new SsNodeLabel();
+                    $ssNodeLabel->node_id = $ssNode->id;
+                    $ssNodeLabel->label_id = $label;
+                    $ssNodeLabel->save();
+                }
+            }
+
             return Response::json(['status' => 'success', 'data' => '', 'message' => '添加成功']);
         } else {
             $view['method_list'] = $this->methodList();
@@ -401,6 +451,7 @@ class AdminController extends Controller
             $view['level_list'] = $this->levelList();
             $view['group_list'] = SsGroup::query()->get();
             $view['country_list'] = Country::query()->orderBy('country_code', 'asc')->get();
+            $view['label_list'] = Label::query()->orderBy('sort', 'desc')->orderBy('id', 'asc')->get();
 
             return Response::view('admin/addNode', $view);
         }
@@ -413,6 +464,7 @@ class AdminController extends Controller
 
         if ($request->method() == 'POST') {
             $name = $request->get('name');
+            $labels = $request->get('labels');
             $group_id = $request->get('group_id');
             $country_code = $request->get('country_code');
             $server = $request->get('server');
@@ -437,35 +489,37 @@ class AdminController extends Controller
             $sort = $request->get('sort');
             $status = $request->get('status');
 
-            $data = [
-                'name'            => $name,
-                'group_id'        => $group_id,
-                'country_code'    => $country_code,
-                'server'          => $server,
-                'desc'            => $desc,
-                'method'          => $method,
-                'protocol'        => $protocol,
-                'protocol_param'  => $protocol_param,
-                'obfs'            => $obfs,
-                'obfs_param'      => $obfs_param,
-                'traffic_rate'    => $traffic_rate,
-                'bandwidth'       => $bandwidth,
-                'traffic'         => $traffic,
-                'monitor_url'     => $monitor_url,
-                'compatible'      => $compatible,
-                'single'          => $single,
-                'single_force'    => $single ? $single_force : 0,
-                'single_port'     => $single ? $single_port : '',
-                'single_passwd'   => $single ? $single_passwd : '',
-                'single_method'   => $single ? $single_method : '',
-                'single_protocol' => $single ? $single_protocol : '',
-                'single_obfs'     => $single ? $single_obfs : '',
-                'sort'            => $sort,
-                'status'          => $status
-            ];
+            DB::beginTransaction();
+            try {
+                $data = [
+                    'name'            => $name,
+                    'group_id'        => $group_id,
+                    'country_code'    => $country_code,
+                    'server'          => $server,
+                    'desc'            => $desc,
+                    'method'          => $method,
+                    'protocol'        => $protocol,
+                    'protocol_param'  => $protocol_param,
+                    'obfs'            => $obfs,
+                    'obfs_param'      => $obfs_param,
+                    'traffic_rate'    => $traffic_rate,
+                    'bandwidth'       => $bandwidth,
+                    'traffic'         => $traffic,
+                    'monitor_url'     => $monitor_url,
+                    'compatible'      => $compatible,
+                    'single'          => $single,
+                    'single_force'    => $single ? $single_force : 0,
+                    'single_port'     => $single ? $single_port : '',
+                    'single_passwd'   => $single ? $single_passwd : '',
+                    'single_method'   => $single ? $single_method : '',
+                    'single_protocol' => $single ? $single_protocol : '',
+                    'single_obfs'     => $single ? $single_obfs : '',
+                    'sort'            => $sort,
+                    'status'          => $status
+                ];
+
+                SsNode::query()->where('id', $id)->update($data);
 
-            $ret = SsNode::query()->where('id', $id)->update($data);
-            if ($ret) {
                 // 建立分组关联
                 if ($group_id) {
                     // 先删除该节点所有关联
@@ -478,18 +532,46 @@ class AdminController extends Controller
                     $ssGroupNode->save();
                 }
 
+                // 生成节点标签
+                if (!empty($labels)) {
+                    // 先删除所有该用户的标签
+                    SsNodeLabel::query()->where('node_id', $id)->delete();
+
+                    foreach ($labels as $label) {
+                        $ssNodeLabel = new SsNodeLabel();
+                        $ssNodeLabel->node_id = $id;
+                        $ssNodeLabel->label_id = $label;
+                        $ssNodeLabel->save();
+                    }
+                }
+
+                DB::commit();
+
                 return Response::json(['status' => 'success', 'data' => '', 'message' => '编辑成功']);
-            } else {
+            } catch (\Exception $e) {
+                DB::rollBack();
+                Log::error('编辑节点信息异常:' . $e->getMessage());
+
                 return Response::json(['status' => 'fail', 'data' => '', 'message' => '编辑失败']);
             }
         } else {
-            $view['node'] = SsNode::query()->where('id', $id)->first();
+            $node = SsNode::query()->with(['label'])->where('id', $id)->first();
+            if ($node) {
+                $labels = [];
+                foreach ($node->label as $vo) {
+                    $labels[] = $vo->label_id;
+                }
+                $node->labels = $labels;
+            }
+
+            $view['node'] = $node;
             $view['method_list'] = $this->methodList();
             $view['protocol_list'] = $this->protocolList();
             $view['obfs_list'] = $this->obfsList();
             $view['level_list'] = $this->levelList();
             $view['group_list'] = SsGroup::query()->get();
             $view['country_list'] = Country::query()->orderBy('country_code', 'asc')->get();
+            $view['label_list'] = Label::query()->orderBy('sort', 'desc')->orderBy('id', 'asc')->get();
 
             return Response::view('admin/editNode', $view);
         }
@@ -1843,4 +1925,70 @@ class AdminController extends Controller
 
         return Response::json(['status' => 'success', 'data' => '', 'message' => "身份切换成功"]);
     }
+
+    // 标签列表
+    public function labelList(Request $request)
+    {
+        $view['labelList'] = Label::query()->paginate(10);
+
+        return Response::view('admin/labelList', $view);
+    }
+
+    // 添加标签
+    public function addLabel(Request $request)
+    {
+        if ($request->isMethod('POST')) {
+            $name = $request->get('name');
+            $sort = $request->get('sort');
+
+            $label = new Label();
+            $label->name = $name;
+            $label->sort = $sort;
+            $label->save();
+
+            return Response::json(['status' => 'success', 'data' => '', 'message' => '添加成功']);
+        } else {
+            return Response::view('admin/addLabel');
+        }
+    }
+
+    // 编辑标签
+    public function editLabel(Request $request)
+    {
+        if ($request->isMethod('POST')) {
+            $id = $request->get('id');
+            $name = $request->get('name');
+            $sort = $request->get('sort');
+
+            Label::query()->where('id', $id)->update(['name' => $name, 'sort' => $sort]);
+
+            return Response::json(['status' => 'success', 'data' => '', 'message' => '添加成功']);
+        } else {
+            $id = $request->get('id');
+            $view['label'] = Label::query()->where('id', $id)->first();
+
+            return Response::view('admin/editLabel', $view);
+        }
+    }
+
+    // 删除标签
+    public function delLabel(Request $request)
+    {
+        $id = $request->get('id');
+
+        DB::beginTransaction();
+        try {
+            Label::query()->where('id', $id)->delete();
+            UserLabel::query()->where('label_id', $id)->delete(); // 删除用户关联
+            SsNodeLabel::query()->where('label_id', $id)->delete(); // 删除节点关联
+
+            DB::commit();
+
+            return Response::json(['status' => 'success', 'data' => '', 'message' => '删除成功']);
+        } catch (\Exception $e) {
+            DB::rollBack();
+
+            return Response::json(['status' => 'fail', 'data' => '', 'message' => '删除失败:' . $e->getMessage()]);
+        }
+    }
 }

+ 11 - 0
app/Http/Controllers/Controller.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Http\Models\UserSubscribe;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Routing\Controller as BaseController;
 use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -29,6 +30,16 @@ class Controller extends BaseController
         return $char;
     }
 
+    public function makeSubscribeCode()
+    {
+        $code = $this->makeRandStr(5);
+        if (UserSubscribe::query()->where('code', $code)->exists()) {
+            $code = $this->makeSubscribeCode();
+        }
+
+        return $code;
+    }
+
     // base64加密(处理URL)
     function base64url_encode($data)
     {

+ 0 - 41
app/Http/Controllers/LocateController.php

@@ -1,41 +0,0 @@
-<?php
-
-namespace App\Http\Controllers;
-
-use App\Http\Models\ArticleLog;
-use Illuminate\Http\Request;
-use Response;
-use Agent;
-
-/**
- * 定位控制器
- * Class LocateController
- * @package App\Http\Controllers
- */
-class LocateController extends Controller
-{
-    // 接收打开文章时上报的定位坐标信息
-    public function locate(Request $request)
-    {
-        $aid = $request->get('aid');
-        $lat = $request->get('lat');
-        $lng = $request->get('lng');
-
-        if (empty($lat) || empty($lng)) {
-            return Response::json(['status' => 'fail', 'data' => '', 'message' => '经纬度不能为空']);
-        }
-
-        // 将坐标写入文章打开记录中
-        $articleLog = new ArticleLog();
-        $articleLog->aid = $aid;
-        $articleLog->lat = $lat;
-        $articleLog->lng = $lng;
-        $articleLog->ip = $request->getClientIp();
-        $articleLog->headers = $request->header('User-Agent');
-        $articleLog->created_at = date('Y-m-d H:i:s');
-        $articleLog->save();
-
-        return Response::json(['status' => 'success', 'data' => '', 'message' => '坐标上报成功']);
-    }
-
-}

+ 2 - 2
app/Http/Controllers/UserController.php

@@ -962,7 +962,7 @@ class UserController extends Controller
         // 如果没有唯一码则生成一个
         $subscribe = UserSubscribe::query()->where('user_id', $user['id'])->first();
         if (empty($subscribe)) {
-            $code = mb_substr(md5($user['id'] . '-' . $user['username']), 8, 12);
+            $code = $this->makeSubscribeCode();
 
             $obj = new UserSubscribe();
             $obj->user_id = $user['id'];
@@ -973,7 +973,7 @@ class UserController extends Controller
             $code = $subscribe->code;
         }
 
-        $view['link'] = self::$config['website_url'] . '/subscribe/' . $code;
+        $view['link'] = self::$config['subscribe_domain'] ? self::$config['subscribe_domain'] . '/s/' . $code : self::$config['website_url'] . '/s/' . $code;
 
         return Response::view('/user/subscribe', $view);
     }

+ 5 - 5
app/Http/Models/ArticleLog.php → app/Http/Models/Label.php

@@ -5,13 +5,13 @@ namespace App\Http\Models;
 use Illuminate\Database\Eloquent\Model;
 
 /**
- * 文章日志
- * Class ArticleLog
+ * 标签
+ * Class Label
  * @package App\Http\Models
  */
-class ArticleLog extends Model
+class Label extends Model
 {
-    protected $table = 'article_log';
+    protected $table = 'label';
     protected $primaryKey = 'id';
-
+    public $timestamps = false;
 }

+ 4 - 0
app/Http/Models/SsNode.php

@@ -14,4 +14,8 @@ class SsNode extends Model
     protected $table = 'ss_node';
     protected $primaryKey = 'id';
 
+    public function label()
+    {
+        return $this->hasMany(SsNodeLabel::class, 'node_id', 'id');
+    }
 }

+ 17 - 0
app/Http/Models/SsNodeLabel.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * 节点标签
+ * Class SsNodeLabel
+ * @package App\Http\Models
+ */
+class SsNodeLabel extends Model
+{
+    protected $table = 'ss_node_label';
+    protected $primaryKey = 'id';
+    public $timestamps = false;
+}

+ 4 - 0
app/Http/Models/User.php

@@ -17,4 +17,8 @@ class User extends Model
     function payment() {
         return $this->hasMany(Payment::class, 'user_id', 'id');
     }
+
+    function label() {
+        return $this->hasMany(UserLabel::class, 'user_id', 'id');
+    }
 }

+ 17 - 0
app/Http/Models/UserLabel.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * 用户标签
+ * Class UserLabel
+ * @package App\Http\Models
+ */
+class UserLabel extends Model
+{
+    protected $table = 'user_label';
+    protected $primaryKey = 'id';
+    public $timestamps = false;
+}

+ 1 - 1
database/migrations/2017_12_29_140319_create_user_subscribe_table.php

@@ -18,7 +18,7 @@ class CreateUserSubscribeTable extends Migration
 
             $table->increments('id');
             $table->integer('user_id')->default('0')->comment('用户ID');
-            $table->string('code', 20)->default('')->nullable()->comment('订阅地址唯一识别码');
+            $table->char('code', 5)->default('')->nullable()->charset('utf8mb4')->collation('utf8mb4_bin')->comment('订阅地址唯一识别码');
             $table->integer('times')->default('0')->comment('地址请求次数');
             $table->tinyInteger('status')->default('1')->comment('状态:0-禁用、1-启用');
             $table->integer('ban_time')->default('0')->comment('封禁时间');

+ 1 - 0
database/seeds/ConfigTableSeeder.php

@@ -58,5 +58,6 @@ class ConfigTableSeeder extends Seeder
         DB::insert("INSERT INTO `config` VALUES ('45', 'paypal_client_secret', '');");
         DB::insert("INSERT INTO `config` VALUES ('46', 'is_free_code', 0);");
         DB::insert("INSERT INTO `config` VALUES ('47', 'is_forbid_robot', 0);");
+        DB::insert("INSERT INTO `config` VALUES ('48', 'subscribe_domain', '');");
     }
 }

+ 19 - 15
readme.md

@@ -12,21 +12,25 @@
 10.账号临近到期、流量不够都会自动发邮件提醒,自动禁用到期、流量异常的账号,自动清除日志
 11.后台一键添加加密方式、混淆、协议、等级
 12.强大的后台一键配置功能
-13.屏蔽常见爬虫
+13.屏蔽常见爬虫、屏蔽机器人
 14.支持单端口多用户
 15.账号、节点24小时和近30天内的流量监控
 16.支持节点订阅功能,可一键封禁账号订阅地址
-17.节点宕机提醒(邮件+ServerChan微信提醒)
-18.Paypal在线支付接口
+17.节点宕机提醒(邮件ServerChan微信提醒)
+18.用户、节点标签化,不同用户可见不同节点
 19.兼容SS、SSRR
 20.支持多国语言
+21.根据根据订阅IP所用宽带,获取最适合的节点
+22.防投毒机制
+23.自动释放端口机制,防止端口被大量长期占用
+24.封IP段
 ````
 
 ## 演示&交流
 ````
-演示站:http://www.ssrpanel.com (用户名:admin 密码:123456,请勿修改密码)
+官方站:http://www.ssrpanel.com
+演示站:http://demo.ssrpanel.com (用户名:admin 密码:123456,请勿修改密码)
 telegram频道:https://t.me/ssrpanel
-telegram群组:https://t.me/chatssrpanel
 ````
 
 ## 捐赠
@@ -70,19 +74,18 @@ chmod -R 777 storage/
 
 ##### 连接数据库
 ````
-先自行创建一个utf8mb4的数据库,
-然后编辑config/database.php,编辑mysql选项中如下配置值:
+先创建一个utf8mb4的数据库,然后编辑config/database.php,编辑mysql选项中如下配置值:
 host、port、database、username、password
 ````
 
 ##### 自动部署
 
-###### 表结构迁移
+###### 迁移(创建表结构)
 ````
 php artisan migrate
 ````
 
-###### 数据播种
+###### 播种(填充数据)
 ````
 php artisan db:seed --class=ConfigTableSeeder
 php artisan db:seed --class=CountryTableSeeder
@@ -91,9 +94,9 @@ php artisan db:seed --class=SsConfigTableSeeder
 php artisan db:seed --class=UserTableSeeder
 ````
 
-##### 手工迁移数据
+##### 手工部署
 ````
-手动将sql/db.sql导入到创建好的数据库
+迁移未经完整测试,可能存在BUG,可以手动将sql/db.sql导入到创建好的数据库
 ````
 
 #### 加入NGINX的URL重写规则
@@ -125,10 +128,9 @@ service nginx restart
 service php-fpm restart
 ````
 
-## 定时任务(发邮件、流量统计、自动任务全部需要用到)
+## 定时任务
 ````
-crontab加入如下命令(请自行修改ssrpanel路径):
-(表示每分钟都执行定时任务,具体什么任务什么时候执行程序里已经定义了,请不要乱改,否则流量统计数据可能出错)
+crontab加入如下命令(请自行修改php、ssrpanel路径):
 * * * * * php /home/wwwroot/ssrpanel/artisan schedule:run >> /dev/null 2>&1
 ````
 
@@ -221,6 +223,8 @@ chmod a+x fix_git.sh && sh fix_git.sh
 
 如果本地自行改了文件,想用回原版代码,请先备份好 config/database.php,然后执行以下命令:
 chmod a+x update.sh && sh update.sh
+
+如果更新完代码各种错误,请先执行一遍 php composer.phar install
 ````
 
 ## 网卡流量监控一键脚本(Vnstat)
@@ -239,7 +243,7 @@ vim user-config.json
         "passwd": "统一认证密码", // 例如 SSRP4ne1,推荐不要出现除大小写字母数字以外的任何字符
         "method": "统一认证加密方式", // 例如 aes-128-ctr
         "protocol": "统一认证协议", // 可选值:orgin、verify_deflate、auth_sha1_v4、auth_aes128_md5(推荐)、auth_aes128_sha1(推荐)、auth_chain_a(强烈推荐)
-        "protocol_param": "#",
+        "protocol_param": "#", // #号前面带上数字,则可以限制在线每个用户的最多在线设备数,仅限 auth_chain_a 协议下有效,例如: 3# 表示限制最多3个设备在线
         "obfs": "tls1.2_ticket_auth", // 可选值:plain、http_simple(该值下客户端可用http_post)、random_head、tls1.2_ticket_auth(强烈推荐)
         "obfs_param": ""
     },

+ 92 - 0
resources/views/admin/addLabel.blade.php

@@ -0,0 +1,92 @@
+@extends('admin.layouts')
+
+@section('css')
+    <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+@endsection
+@section('title', '控制面板')
+@section('content')
+    <!-- BEGIN CONTENT BODY -->
+    <div class="page-content" style="padding-top:0;">
+        <!-- BEGIN PAGE BASE CONTENT -->
+        <div class="row">
+            <div class="col-md-12">
+                @if (Session::has('errorMsg'))
+                    <div class="alert alert-danger">
+                        <button class="close" data-close="alert"></button>
+                        <strong>错误:</strong> {{Session::get('errorMsg')}}
+                    </div>
+                @endif
+                <!-- BEGIN PORTLET-->
+                <div class="portlet light form-fit bordered">
+                    <div class="portlet-title">
+                        <div class="caption">
+                            <span class="caption-subject font-dark sbold uppercase">添加标签</span>
+                        </div>
+                        <div class="actions"></div>
+                    </div>
+                    <div class="portlet-body form">
+                        <!-- BEGIN FORM-->
+                        <form action="{{url('admin/addLabel')}}" method="post" enctype="multipart/form-data" class="form-horizontal" onsubmit="return doSubmit();">
+                            <div class="form-body">
+                                <div class="form-group">
+                                    <label class="control-label col-md-1">名称</label>
+                                    <div class="col-md-6">
+                                        <input type="text" class="form-control" name="name" id="name" placeholder="" autofocus required>
+                                        <input type="hidden" name="_token" value="{{csrf_token()}}">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label class="control-label col-md-1">排序</label>
+                                    <div class="col-md-6">
+                                        <input type="text" class="form-control" name="sort" id="sort" value="0" required />
+                                        <span class="help-block"> 值越高显示时越靠前 </span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-actions">
+                                <div class="row">
+                                    <div class="col-md-offset-6">
+                                        <button type="submit" class="btn green">提 交</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </form>
+                        <!-- END FORM-->
+                    </div>
+                </div>
+                <!-- END PORTLET-->
+            </div>
+        </div>
+        <!-- END PAGE BASE CONTENT -->
+    </div>
+    <!-- END CONTENT BODY -->
+@endsection
+@section('script')
+    <script src="/js/layer/layer.js" type="text/javascript"></script>
+
+    <script type="text/javascript">
+        // ajax同步提交
+        function doSubmit() {
+            var _token = '{{csrf_token()}}';
+            var name = $('#name').val();
+            var sort = $('#sort').val();
+
+            $.ajax({
+                type: "POST",
+                url: "{{url('admin/addLabel')}}",
+                async: false,
+                data: {_token:_token, name: name, sort:sort},
+                dataType: 'json',
+                success: function (ret) {
+                    layer.msg(ret.message, {time:1000}, function() {
+                        if (ret.status == 'success') {
+                            window.location.href = '{{url('admin/labelList')}}';
+                        }
+                    });
+                }
+            });
+
+            return false;
+        }
+    </script>
+@endsection

+ 21 - 1
resources/views/admin/addNode.blade.php

@@ -2,6 +2,8 @@
 
 @section('css')
     <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2-bootstrap.min.css" rel="stylesheet" type="text/css" />
 @endsection
 @section('title', '控制面板')
 @section('content')
@@ -42,6 +44,16 @@
                                                             <input type="text" class="form-control" name="server" id="server" placeholder="域名或IP地址" required>
                                                         </div>
                                                     </div>
+                                                    <div class="form-group">
+                                                        <label for="status" class="col-md-3 control-label">标签</label>
+                                                        <div class="col-md-8">
+                                                            <select id="labels" class="form-control select2-multiple" name="labels[]" multiple>
+                                                                @foreach($label_list as $label)
+                                                                    <option value="{{$label->id}}">{{$label->name}}</option>
+                                                                @endforeach
+                                                            </select>
+                                                        </div>
+                                                    </div>
                                                     <div class="form-group">
                                                         <label for="group_id" class="col-md-3 control-label"> 所属分组 </label>
                                                         <div class="col-md-8">
@@ -285,12 +297,20 @@
     <!-- END CONTENT BODY -->
 @endsection
 @section('script')
+    <script src="/assets/global/plugins/select2/js/select2.full.min.js" type="text/javascript"></script>
     <script src="/js/layer/layer.js" type="text/javascript"></script>
 
     <script type="text/javascript">
+        // 用户标签选择器
+        $('#labels').select2({
+            placeholder: '设置后则可见相同标签的节点',
+            allowClear: true
+        });
+
         // ajax同步提交
         function do_submit() {
             var name = $('#name').val();
+            var labels = $("#labels").val();
             var group_id = $("#group_id option:selected").val();
             var country_code = $("#country_code option:selected").val();
             var server = $('#server').val();
@@ -319,7 +339,7 @@
                 type: "POST",
                 url: "{{url('admin/addNode')}}",
                 async: false,
-                data: {_token:'{{csrf_token()}}', name: name, group_id:group_id, country_code:country_code, server:server, desc:desc, method:method, traffic_rate:traffic_rate, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, bandwidth:bandwidth, traffic:traffic, monitor_url:monitor_url, compatible:compatible, single:single, single_force:single_force, single_port:single_port, single_passwd:single_passwd, single_method:single_method, single_protocol:single_protocol, single_obfs:single_obfs, sort:sort, status:status},
+                data: {_token:'{{csrf_token()}}', name: name, labels:labels, group_id:group_id, country_code:country_code, server:server, desc:desc, method:method, traffic_rate:traffic_rate, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, bandwidth:bandwidth, traffic:traffic, monitor_url:monitor_url, compatible:compatible, single:single, single_force:single_force, single_port:single_port, single_passwd:single_passwd, single_method:single_method, single_protocol:single_protocol, single_obfs:single_obfs, sort:sort, status:status},
                 dataType: 'json',
                 success: function (ret) {
                     layer.msg(ret.message, {time:1000}, function() {

+ 22 - 1
resources/views/admin/addUser.blade.php

@@ -2,6 +2,8 @@
 
 @section('css')
     <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2-bootstrap.min.css" rel="stylesheet" type="text/css" />
 @endsection
 @section('title', '控制面板')
 @section('content')
@@ -107,6 +109,17 @@
                                                 </div>
                                             </div>
                                             <hr>
+                                            <div class="form-group">
+                                                <label for="status" class="col-md-3 control-label">标签</label>
+                                                <div class="col-md-8">
+                                                    <select id="labels" class="form-control select2-multiple" name="labels[]" multiple>
+                                                        @foreach($label_list as $label)
+                                                            <option value="{{$label->id}}">{{$label->name}}</option>
+                                                        @endforeach
+                                                    </select>
+                                                </div>
+                                            </div>
+                                            <hr>
                                             <div class="form-group">
                                                 <label for="gender" class="col-md-3 control-label">性别</label>
                                                 <div class="col-md-8">
@@ -279,9 +292,16 @@
 @section('script')
     <script src="/assets/global/plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js" type="text/javascript"></script>
     <script src="/assets/global/plugins/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js" type="text/javascript"></script>
+    <script src="/assets/global/plugins/select2/js/select2.full.min.js" type="text/javascript"></script>
     <script src="/js/layer/layer.js" type="text/javascript"></script>
 
     <script type="text/javascript">
+        // 用户标签选择器
+        $('#labels').select2({
+            placeholder: '设置后则可见相同标签的节点',
+            allowClear: true
+        });
+
         // 有效期
         $('.input-daterange input').each(function() {
             $(this).datepicker({
@@ -299,6 +319,7 @@
             var password = $('#password').val();
             var usage = $("input:radio[name='usage']:checked").val();
             var pay_way = $("input:radio[name='pay_way']:checked").val();
+            var labels = $('#labels').val();
             var enable_time = $('#enable_time').val();
             var expire_time = $('#expire_time').val();
             var gender = $('#gender').val();
@@ -323,7 +344,7 @@
                 type: "POST",
                 url: "{{url('admin/addUser')}}",
                 async: false,
-                data: {_token:_token, username: username, password:password, usage:usage, pay_way:pay_way, enable_time:enable_time, expire_time:expire_time, gender:gender, wechat:wechat, qq:qq, is_admin:is_admin, remark:remark, level:level, port:port, passwd:passwd, method:method, transfer_enable:transfer_enable, enable:enable, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, speed_limit_per_con:speed_limit_per_con, speed_limit_per_user:speed_limit_per_user},
+                data: {_token:_token, username: username, password:password, usage:usage, pay_way:pay_way, labels:labels, enable_time:enable_time, expire_time:expire_time, gender:gender, wechat:wechat, qq:qq, is_admin:is_admin, remark:remark, level:level, port:port, passwd:passwd, method:method, transfer_enable:transfer_enable, enable:enable, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, speed_limit_per_con:speed_limit_per_con, speed_limit_per_user:speed_limit_per_user},
                 dataType: 'json',
                 success: function (ret) {
                     layer.msg(ret.message, {time:1000}, function() {

+ 93 - 0
resources/views/admin/editLabel.blade.php

@@ -0,0 +1,93 @@
+@extends('admin.layouts')
+
+@section('css')
+    <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+@endsection
+@section('title', '控制面板')
+@section('content')
+    <!-- BEGIN CONTENT BODY -->
+    <div class="page-content" style="padding-top:0;">
+        <!-- BEGIN PAGE BASE CONTENT -->
+        <div class="row">
+            <div class="col-md-12">
+                @if (Session::has('errorMsg'))
+                    <div class="alert alert-danger">
+                        <button class="close" data-close="alert"></button>
+                        <strong>错误:</strong> {{Session::get('errorMsg')}}
+                    </div>
+                @endif
+                <!-- BEGIN PORTLET-->
+                <div class="portlet light form-fit bordered">
+                    <div class="portlet-title">
+                        <div class="caption">
+                            <span class="caption-subject font-darm sbold uppercase">编辑文章</span>
+                        </div>
+                        <div class="actions"></div>
+                    </div>
+                    <div class="portlet-body form">
+                        <!-- BEGIN FORM-->
+                        <form action="{{url('admin/editLabel')}}" method="post" enctype="multipart/form-data" class="form-horizontal" onsubmit="return doSubmit();">
+                            <div class="form-body">
+                                <div class="form-group">
+                                    <label class="control-label col-md-1">标题</label>
+                                    <div class="col-md-6">
+                                        <input type="text" class="form-control" name="name" value="{{$label->name}}" id="name" placeholder="" autofocus required>
+                                        <input type="hidden" name="_token" value="{{csrf_token()}}">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label class="control-label col-md-1">排序</label>
+                                    <div class="col-md-6">
+                                        <input type="text" class="form-control" name="sort" value="{{$label->sort}}" id="sort" required />
+                                        <span class="help-block"> 值越高显示时越靠前 </span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-actions">
+                                <div class="row">
+                                    <div class="col-md-offset-6">
+                                        <button type="submit" class="btn green">提 交</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </form>
+                        <!-- END FORM-->
+                    </div>
+                </div>
+                <!-- END PORTLET-->
+            </div>
+        </div>
+        <!-- END PAGE BASE CONTENT -->
+    </div>
+    <!-- END CONTENT BODY -->
+@endsection
+@section('script')
+    <script src="/js/layer/layer.js" type="text/javascript"></script>
+
+    <script type="text/javascript">
+        // ajax同步提交
+        function doSubmit() {
+            var _token = '{{csrf_token()}}';
+            var id = '{{$label->id}}';
+            var name = $('#name').val();
+            var sort = $('#sort').val();
+
+            $.ajax({
+                type: "POST",
+                url: "{{url('admin/editLabel')}}",
+                async: false,
+                data: {_token:_token, id:id, name: name, sort:sort},
+                dataType: 'json',
+                success: function (ret) {
+                    layer.msg(ret.message, {time:1000}, function() {
+                        if (ret.status == 'success') {
+                            window.location.href = '{{url('admin/labelList')}}';
+                        }
+                    });
+                }
+            });
+
+            return false;
+        }
+    </script>
+@endsection

+ 21 - 1
resources/views/admin/editNode.blade.php

@@ -2,6 +2,8 @@
 
 @section('css')
     <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2-bootstrap.min.css" rel="stylesheet" type="text/css" />
 @endsection
 @section('title', '控制面板')
 @section('content')
@@ -40,6 +42,16 @@
                                                             <input type="text" class="form-control" name="server" value="{{$node->server}}" id="server" placeholder="域名或IP地址" required>
                                                         </div>
                                                     </div>
+                                                    <div class="form-group">
+                                                        <label for="status" class="col-md-3 control-label">标签</label>
+                                                        <div class="col-md-8">
+                                                            <select id="labels" class="form-control select2-multiple" name="labels[]" multiple>
+                                                                @foreach($label_list as $label)
+                                                                    <option value="{{$label->id}}" @if(in_array($label->id, $node->labels)) selected @endif>{{$label->name}}</option>
+                                                                @endforeach
+                                                            </select>
+                                                        </div>
+                                                    </div>
                                                     <div class="form-group">
                                                         <label for="group_id" class="col-md-3 control-label"> 所属分组 </label>
                                                         <div class="col-md-8">
@@ -283,14 +295,22 @@
     <!-- END CONTENT BODY -->
 @endsection
 @section('script')
+    <script src="/assets/global/plugins/select2/js/select2.full.min.js" type="text/javascript"></script>
     <script src="/js/layer/layer.js" type="text/javascript"></script>
 
     <script type="text/javascript">
+        // 用户标签选择器
+        $('#labels').select2({
+            placeholder: '设置后则可见相同标签的节点',
+            allowClear: true
+        });
+
         // ajax同步提交
         function do_submit() {
             var _token = '{{csrf_token()}}';
             var id = '{{Request::get('id')}}';
             var name = $('#name').val();
+            var labels = $("#labels").val();
             var group_id = $("#group_id option:selected").val();
             var country_code = $("#country_code option:selected").val();
             var server = $('#server').val();
@@ -319,7 +339,7 @@
                 type: "POST",
                 url: "{{url('admin/editNode')}}",
                 async: false,
-                data: {_token:_token, id:id, name: name, group_id:group_id, country_code:country_code, server:server, desc:desc, method:method, traffic_rate:traffic_rate, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, bandwidth:bandwidth, traffic:traffic, monitor_url:monitor_url, compatible:compatible, single:single, single_force:single_force, single_port:single_port, single_passwd:single_passwd, single_method:single_method, single_protocol:single_protocol, single_obfs:single_obfs, sort:sort, status:status},
+                data: {_token:_token, id:id, name: name, labels:labels, group_id:group_id, country_code:country_code, server:server, desc:desc, method:method, traffic_rate:traffic_rate, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, bandwidth:bandwidth, traffic:traffic, monitor_url:monitor_url, compatible:compatible, single:single, single_force:single_force, single_port:single_port, single_passwd:single_passwd, single_method:single_method, single_protocol:single_protocol, single_obfs:single_obfs, sort:sort, status:status},
                 dataType: 'json',
                 success: function (ret) {
                     layer.msg(ret.message, {time:1000}, function() {

+ 22 - 1
resources/views/admin/editUser.blade.php

@@ -2,6 +2,8 @@
 
 @section('css')
     <link href="/assets/global/plugins/bootstrap-datepicker/css/bootstrap-datepicker3.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/select2/css/select2-bootstrap.min.css" rel="stylesheet" type="text/css" />
 @endsection
 @section('title', '控制面板')
 @section('content')
@@ -147,6 +149,17 @@
                                                 </div>
                                             </div>
                                             <hr>
+                                            <div class="form-group">
+                                                <label for="status" class="col-md-3 control-label">标签</label>
+                                                <div class="col-md-8">
+                                                    <select id="labels" class="form-control select2-multiple" name="labels[]" multiple>
+                                                        @foreach($label_list as $label)
+                                                            <option value="{{$label->id}}" @if(in_array($label->id, $user->labels)) selected @endif>{{$label->name}}</option>
+                                                        @endforeach
+                                                    </select>
+                                                </div>
+                                            </div>
+                                            <hr>
                                             <div class="form-group">
                                                 <label for="gender" class="col-md-3 control-label">性别</label>
                                                 <div class="col-md-8">
@@ -350,9 +363,16 @@
 @section('script')
     <script src="/assets/global/plugins/bootstrap-datepicker/js/bootstrap-datepicker.min.js" type="text/javascript"></script>
     <script src="/assets/global/plugins/bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js" type="text/javascript"></script>
+    <script src="/assets/global/plugins/select2/js/select2.full.min.js" type="text/javascript"></script>
     <script src="/js/layer/layer.js" type="text/javascript"></script>
 
     <script type="text/javascript">
+        // 用户标签选择器
+        $('#labels').select2({
+            placeholder: '设置后则可见相同标签的节点',
+            allowClear: true
+        });
+
         // 切换用户身份
         function switchToUser() {
             $.ajax({
@@ -394,6 +414,7 @@
             var balance = $('#balance').val();
             var score = $('#score').val();
             var status = $('#status').val();
+            var labels = $('#labels').val();
             var enable_time = $('#enable_time').val();
             var expire_time = $('#expire_time').val();
             var gender = $('#gender').val();
@@ -418,7 +439,7 @@
                 type: "POST",
                 url: "{{url('admin/editUser')}}",
                 async: false,
-                data: {_token:_token, id:id, username: username, password:password, usage:usage, pay_way:pay_way, balance:balance, score:score, status:status, enable_time:enable_time, expire_time:expire_time, gender:gender, wechat:wechat, qq:qq, is_admin:is_admin, remark:remark, level:level, port:port, passwd:passwd, method:method, transfer_enable:transfer_enable, enable:enable, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, speed_limit_per_con:speed_limit_per_con, speed_limit_per_user:speed_limit_per_user},
+                data: {_token:_token, id:id, username: username, password:password, usage:usage, pay_way:pay_way, balance:balance, score:score, status:status, labels:labels, enable_time:enable_time, expire_time:expire_time, gender:gender, wechat:wechat, qq:qq, is_admin:is_admin, remark:remark, level:level, port:port, passwd:passwd, method:method, transfer_enable:transfer_enable, enable:enable, protocol:protocol, protocol_param:protocol_param, obfs:obfs, obfs_param:obfs_param, speed_limit_per_con:speed_limit_per_con, speed_limit_per_user:speed_limit_per_user},
                 dataType: 'json',
                 success: function (ret) {
                     layer.msg(ret.message, {time:1000}, function() {

+ 110 - 0
resources/views/admin/labelList.blade.php

@@ -0,0 +1,110 @@
+@extends('admin.layouts')
+
+@section('css')
+    <link href="/assets/global/plugins/datatables/datatables.min.css" rel="stylesheet" type="text/css" />
+    <link href="/assets/global/plugins/datatables/plugins/bootstrap/datatables.bootstrap.css" rel="stylesheet" type="text/css" />
+@endsection
+@section('title', '控制面板')
+@section('content')
+    <!-- BEGIN CONTENT BODY -->
+    <div class="page-content" style="padding-top:0;">
+        <!-- BEGIN PAGE BASE CONTENT -->
+        <div class="row">
+            <div class="col-md-12">
+                <!-- BEGIN EXAMPLE TABLE PORTLET-->
+                <div class="portlet light bordered">
+                    <div class="portlet-title">
+                        <div class="caption font-dark">
+                            <span class="caption-subject bold uppercase"> 标签列表 </span>
+                        </div>
+                        <div class="actions">
+                            <div class="btn-group">
+                                <button class="btn sbold blue" onclick="addLabel()"> 添加标签 </button>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="portlet-body">
+                        <div class="table-scrollable">
+                            <table class="table table-striped table-bordered table-hover table-checkable order-column">
+                                <thead>
+                                <tr>
+                                    <th> # </th>
+                                    <th> 名称 </th>
+                                    <th> 排序 </th>
+                                    <th> 操作 </th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @if($labelList->isEmpty())
+                                    <tr>
+                                        <td colspan="4">暂无数据</td>
+                                    </tr>
+                                @else
+                                    @foreach($labelList as $label)
+                                        <tr class="odd gradeX">
+                                            <td> {{$label->id}} </td>
+                                            <td> {{$label->name}} </td>
+                                            <td> {{$label->sort}} </td>
+                                            <td>
+                                                <button type="button" class="btn btn-sm blue btn-outline" onclick="editLabel('{{$label->id}}')">
+                                                    <i class="fa fa-pencil"></i>
+                                                </button>
+                                                <button type="button" class="btn btn-sm red btn-outline" onclick="delLabel('{{$label->id}}')">
+                                                    <i class="fa fa-trash"></i>
+                                                </button>
+                                            </td>
+                                        </tr>
+                                    @endforeach
+                                @endif
+                                </tbody>
+                            </table>
+                        </div>
+                        <div class="row">
+                            <div class="col-md-4 col-sm-4">
+                                <div class="dataTables_info" role="status" aria-live="polite">共 {{$labelList->total()}} 个标签</div>
+                            </div>
+                            <div class="col-md-8 col-sm-8">
+                                <div class="dataTables_paginate paging_bootstrap_full_number pull-right">
+                                    {{ $labelList->links() }}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <!-- END EXAMPLE TABLE PORTLET-->
+            </div>
+        </div>
+        <!-- END PAGE BASE CONTENT -->
+    </div>
+    <!-- END CONTENT BODY -->
+@endsection
+@section('script')
+    <script src="/js/layer/layer.js" type="text/javascript"></script>
+
+    <script type="text/javascript">
+        // 添加标签
+        function addLabel() {
+            window.location.href = '{{url('admin/addLabel')}}';
+        }
+
+        // 编辑标签
+        function editLabel(id) {
+            window.location.href = '{{url('admin/editLabel?id=')}}' + id + '&page=' + '{{Request::get('page', 1)}}';
+        }
+
+        // 删除标签
+        function delLabel(id) {
+            layer.confirm('确定删除标签?', {icon: 2, title:'警告'}, function(index) {
+                $.post("{{url('admin/delLabel')}}", {id:id, _token:'{{csrf_token()}}'}, function(ret) {
+                    layer.msg(ret.message, {time:1000}, function() {
+                        if (ret.status == 'success') {
+                            window.location.reload();
+                        }
+                    });
+                });
+
+                layer.close(index);
+            });
+        }
+    </script>
+@endsection

+ 6 - 0
resources/views/admin/layouts.blade.php

@@ -141,6 +141,12 @@
                         <span class="title">工单管理</span>
                     </a>
                 </li>
+                <li class="nav-item {{in_array(Request::path(), ['admin/labelList', 'admin/addLabel', 'admin/editLabel']) ? 'active open' : ''}}">
+                    <a href="{{url('admin/labelList')}}" class="nav-link nav-toggle">
+                        <i class="fa fa-sticky-note-o"></i>
+                        <span class="title">标签管理</span>
+                    </a>
+                </li>
                 <li class="nav-item {{in_array(Request::path(), ['admin/articleList', 'admin/addArticle', 'admin/editArticle', 'admin/articleLogList']) ? 'active open' : ''}}">
                     <a href="javascript:;" class="nav-link nav-toggle">
                         <i class="icon-docs"></i>

+ 36 - 12
resources/views/admin/system.blade.php

@@ -216,6 +216,13 @@
                                                         </div>
                                                     </div>
                                                     <div class="form-group">
+                                                        <div class="col-md-6">
+                                                            <label for="is_forbid_robot" class="col-md-3 control-label">阻止机器人访问</label>
+                                                            <div class="col-md-9">
+                                                                <input type="checkbox" class="make-switch" @if($is_forbid_robot) checked @endif id="is_forbid_robot" data-on-color="success" data-off-color="danger" data-on-text="启用" data-off-text="关闭">
+                                                                <span class="help-block"> 如果是机器人、爬虫、代理访问网站则会抛出403错误 </span>
+                                                            </div>
+                                                        </div>
                                                         <div class="col-md-6">
                                                             <label for="active_times" class="col-md-3 control-label">激活账号次数</label>
                                                             <div class="col-md-9">
@@ -228,28 +235,32 @@
                                                                 <span class="help-block"> 24小时内可以通过邮件激活账号次数 </span>
                                                             </div>
                                                         </div>
+                                                    </div>
+                                                    <div class="form-group">
                                                         <div class="col-md-6">
-                                                            <label for="subscribe_max" class="col-md-3 control-label">订阅节点数</label>
+                                                            <label for="subscribe_domain" class="col-md-3 control-label">节点订阅地址</label>
                                                             <div class="col-md-9">
                                                                 <div class="input-group">
-                                                                    <input class="form-control" type="text" name="subscribe_max" value="{{$subscribe_max}}" id="subscribe_max" />
+                                                                    <input class="form-control" type="text" name="subscribe_domain" value="{{$subscribe_domain}}" id="subscribe_domain" />
                                                                     <span class="input-group-btn">
-                                                                    <button class="btn btn-success" type="button" onclick="setSubscribeMax()">修改</button>
-                                                                </span>
+                                                                        <button class="btn btn-success" type="button" onclick="setSubscribeDomain()">修改</button>
+                                                                    </span>
                                                                 </div>
-                                                                <span class="help-block"> 客户端订阅时随机取得几个节点 </span>
+                                                                <span class="help-block"> (推荐)防止网站域名被投毒后用户无法正常订阅,需带http://或https:// </span>
                                                             </div>
                                                         </div>
-                                                    </div>
-                                                    <div class="form-group">
                                                         <div class="col-md-6">
-                                                            <label for="is_forbid_robot" class="col-md-3 control-label">阻止机器人访问</label>
+                                                            <label for="subscribe_max" class="col-md-3 control-label">订阅节点数</label>
                                                             <div class="col-md-9">
-                                                                <input type="checkbox" class="make-switch" @if($is_forbid_robot) checked @endif id="is_forbid_robot" data-on-color="success" data-off-color="danger" data-on-text="启用" data-off-text="关闭">
-                                                                <span class="help-block"> 如果是机器人、爬虫、代理访问网站则会抛出403错误 </span>
+                                                                <div class="input-group">
+                                                                    <input class="form-control" type="text" name="subscribe_max" value="{{$subscribe_max}}" id="subscribe_max" />
+                                                                    <span class="input-group-btn">
+                                                                        <button class="btn btn-success" type="button" onclick="setSubscribeMax()">修改</button>
+                                                                    </span>
+                                                                </div>
+                                                                <span class="help-block"> 客户端订阅时随机取得几个节点 </span>
                                                             </div>
                                                         </div>
-                                                        <div class="col-md-6"></div>
                                                     </div>
                                                 </div>
                                             </form>
@@ -451,7 +462,7 @@
                                                             <label for="is_clear_log" class="col-md-3 control-label">自动清除日志</label>
                                                             <div class="col-md-9">
                                                                 <input type="checkbox" class="make-switch" @if($is_clear_log) checked @endif id="is_clear_log" data-on-color="success" data-off-color="danger" data-on-text="启用" data-off-text="关闭">
-                                                                <span class="help-block"> 启用后自动清除无用日志(推荐) </span>
+                                                                <span class="help-block"> (推荐)启用后自动清除无用日志 </span>
                                                             </div>
                                                         </div>
                                                         <div class="col-md-6">
@@ -1197,6 +1208,19 @@
             });
         }
 
+        // 设置激活用户次数
+        function setSubscribeDomain() {
+            var subscribe_domain = $("#subscribe_domain").val();
+
+            $.post("{{url('admin/setConfig')}}", {_token:'{{csrf_token()}}', name:'subscribe_domain', value:subscribe_domain}, function (ret) {
+                layer.msg(ret.message, {time:1000}, function() {
+                    if (ret.status == 'fail') {
+                        window.location.reload();
+                    }
+                });
+            });
+        }
+
         // 设置节点订阅随机展示节点数
         function setSubscribeMax() {
             var subscribe_max = $("#subscribe_max").val();

+ 9 - 6
routes/web.php

@@ -1,6 +1,6 @@
 <?php
 
-Route::get('subscribe/{code}', 'SubscribeController@index'); // 节点订阅地址
+Route::get('s/{code}', 'SubscribeController@index'); // 节点订阅地址
 Route::post('locate', 'LocateController@locate'); // 上报文章打开时的定位
 
 Route::group(['middleware' => ['forbidden']], function () {
@@ -27,14 +27,17 @@ Route::group(['middleware' => ['forbidden', 'user', 'admin']], function () {
     Route::post('admin/delNode', 'AdminController@delNode'); // 删除节点
     Route::get('admin/nodeMonitor', 'AdminController@nodeMonitor'); // 节点流量监控
     Route::get('admin/articleList', 'AdminController@articleList'); // 文章列表
-    Route::get('admin/articleLogList', 'AdminController@articleLogList'); // 文章访问日志列表
     Route::any('admin/addArticle', 'AdminController@addArticle'); // 添加文章
     Route::any('admin/editArticle', 'AdminController@editArticle'); // 编辑文章
     Route::post('admin/delArticle', 'AdminController@delArticle'); // 删除文章
-    Route::get('admin/groupList', 'AdminController@groupList'); // 文章列表
-    Route::any('admin/addGroup', 'AdminController@addGroup'); // 添加文章
-    Route::any('admin/editGroup', 'AdminController@editGroup'); // 编辑文章
-    Route::post('admin/delGroup', 'AdminController@delGroup'); // 删除文章
+    Route::get('admin/groupList', 'AdminController@groupList'); // 分组列表
+    Route::any('admin/addGroup', 'AdminController@addGroup'); // 添加分组
+    Route::any('admin/editGroup', 'AdminController@editGroup'); // 编辑分组
+    Route::post('admin/delGroup', 'AdminController@delGroup'); // 删除分组
+    Route::get('admin/labelList', 'AdminController@labelList'); // 标签列表
+    Route::any('admin/addLabel', 'AdminController@addLabel'); // 添加标签
+    Route::any('admin/editLabel', 'AdminController@editLabel'); // 编辑标签
+    Route::post('admin/delLabel', 'AdminController@delLabel'); // 删除标签
     Route::get('ticket/ticketList', 'TicketController@ticketList'); // 工单列表
     Route::any('ticket/replyTicket', 'TicketController@replyTicket'); // 回复工单
     Route::post('ticket/closeTicket', 'TicketController@closeTicket'); // 关闭工单

+ 52 - 1
sql/db.sql

@@ -82,6 +82,20 @@ CREATE TABLE `ss_node_online_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='节点在线信息';
 
 
+-- ----------------------------
+-- Table structure for `ss_node_label`
+-- ----------------------------
+CREATE TABLE `ss_node_label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `node_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
+  `label_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID',
+  PRIMARY KEY (`id`),
+  KEY `idx` (`node_id`,`label_id`),
+  KEY `idx_node_id` (`node_id`),
+  KEY `idx_label_id` (`label_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点标签';
+
+
 -- ----------------------------
 -- Table structure for `user`
 -- ----------------------------
@@ -301,6 +315,7 @@ INSERT INTO `config` VALUES ('44', 'paypal_client_id', '');
 INSERT INTO `config` VALUES ('45', 'paypal_client_secret', '');
 INSERT INTO `config` VALUES ('46', 'is_free_code', 0);
 INSERT INTO `config` VALUES ('47', 'is_forbid_robot', 0);
+INSERT INTO `config` VALUES ('48', 'subscribe_domain', '');
 
 
 -- ----------------------------
@@ -336,6 +351,28 @@ CREATE TABLE `invite` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='邀请码表';
 
 
+-- ----------------------------
+-- Table structure for `label`
+-- ----------------------------
+CREATE TABLE `label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序值',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签';
+
+
+-- ----------------------------
+-- Records of label
+-- ----------------------------
+INSERT INTO `label` VALUES ('1', '电信', '0');
+INSERT INTO `label` VALUES ('2', '联通', '0');
+INSERT INTO `label` VALUES ('3', '移动', '0');
+INSERT INTO `label` VALUES ('4', '教育网', '0');
+INSERT INTO `label` VALUES ('5', '其他网络', '0');
+INSERT INTO `label` VALUES ('6', '免费体验', '0');
+
+
 -- ----------------------------
 -- Table structure for `verify`
 -- ----------------------------
@@ -587,7 +624,7 @@ CREATE TABLE `email_log` (
 CREATE TABLE `user_subscribe` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
-  `code` varchar(255) DEFAULT '' COMMENT '订阅地址唯一识别码',
+  `code` char(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '订阅地址唯一识别码',
   `times` int(11) NOT NULL DEFAULT '0' COMMENT '地址请求次数',
   `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用、1-启用',
   `ban_time` int(11) NOT NULL DEFAULT '0' COMMENT '封禁时间',
@@ -698,6 +735,20 @@ CREATE TABLE `user_ban_log` (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户封禁日志';
 
 
+-- ----------------------------
+-- Table structure for `user_label`
+-- ----------------------------
+CREATE TABLE `user_label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
+  `label_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID',
+  PRIMARY KEY (`id`),
+  KEY `idx` (`user_id`,`label_id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_label_id` (`label_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户标签';
+
+
 -- ----------------------------
 -- Table structure for `country`
 -- ----------------------------

+ 43 - 0
sql/update/20180205.sql

@@ -0,0 +1,43 @@
+-- 节点订阅地址
+INSERT INTO `config` VALUES ('48', 'subscribe_domain', '');
+
+-- 节点订阅地址缩短并改为可以识别大小写
+ALTER TABLE `user_subscribe`
+MODIFY COLUMN `code`  char(20) BINARY CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '' COMMENT '订阅地址唯一识别码' AFTER `user_id`;
+
+-- 标签
+CREATE TABLE `label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序值',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标签';
+
+INSERT INTO `label` VALUES ('1', '电信', '0');
+INSERT INTO `label` VALUES ('2', '联通', '0');
+INSERT INTO `label` VALUES ('3', '移动', '0');
+INSERT INTO `label` VALUES ('4', '教育网', '0');
+INSERT INTO `label` VALUES ('5', '其他网络', '0');
+INSERT INTO `label` VALUES ('6', '免费体验', '0');
+
+-- 用户标签
+CREATE TABLE `user_label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
+  `label_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID',
+  PRIMARY KEY (`id`),
+  KEY `idx` (`user_id`,`label_id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_label_id` (`label_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户标签';
+
+-- 节点标签
+CREATE TABLE `ss_node_label` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `node_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
+  `label_id` int(11) NOT NULL DEFAULT '0' COMMENT '标签ID',
+  PRIMARY KEY (`id`),
+  KEY `idx` (`node_id`,`label_id`),
+  KEY `idx_node_id` (`node_id`),
+  KEY `idx_label_id` (`label_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点标签';