Răsfoiți Sursa

添加节点地图 + 修复系统设置页面元素错误+代码简写规范化

兔姬桑 4 ani în urmă
părinte
comite
f50bf334ea
88 a modificat fișierele cu 978 adăugiri și 387 ștergeri
  1. 4 9
      app/Components/Helpers.php
  2. 1 3
      app/Components/IPIP.php
  3. 1 2
      app/Components/Namesilo.php
  4. 3 5
      app/Components/NetworkDetection.php
  5. 2 4
      app/Components/PushNotification.php
  6. 9 14
      app/Components/QQInfo.php
  7. 2 4
      app/Console/Commands/AutoJob.php
  8. 5 6
      app/Console/Commands/AutoPingNode.php
  9. 1 2
      app/Console/Commands/AutoStatisticsNodeDailyTraffic.php
  10. 1 2
      app/Console/Commands/AutoStatisticsNodeHourlyTraffic.php
  11. 2 4
      app/Console/Commands/AutoStatisticsUserDailyTraffic.php
  12. 2 4
      app/Console/Commands/AutoStatisticsUserHourlyTraffic.php
  13. 1 2
      app/Console/Commands/UserExpireAutoWarning.php
  14. 1 2
      app/Console/Commands/UserTrafficAutoWarning.php
  15. 3 6
      app/Console/Commands/updateTicket.php
  16. 1 2
      app/Console/Commands/updateUserLevel.php
  17. 5 5
      app/Console/Commands/updateUserName.php
  18. 1 2
      app/Console/Commands/upgradeUserResetTime.php
  19. 120 0
      app/Http/Controllers/Admin/GroupController.php
  20. 1 2
      app/Http/Controllers/Admin/MarketingController.php
  21. 9 10
      app/Http/Controllers/Admin/RuleController.php
  22. 2 2
      app/Http/Controllers/Admin/ShopController.php
  23. 6 6
      app/Http/Controllers/Admin/ToolsController.php
  24. 36 31
      app/Http/Controllers/AdminController.php
  25. 7 2
      app/Http/Controllers/Api/LoginController.php
  26. 10 13
      app/Http/Controllers/Api/WebApi/BaseController.php
  27. 6 1
      app/Http/Controllers/Api/WebApi/TrojanController.php
  28. 6 1
      app/Http/Controllers/Api/WebApi/V2RayController.php
  29. 7 2
      app/Http/Controllers/Api/WebApi/VNetController.php
  30. 16 7
      app/Http/Controllers/AuthController.php
  31. 2 1
      app/Http/Controllers/Controller.php
  32. 1 2
      app/Http/Controllers/Gateway/AbstractPayment.php
  33. 5 4
      app/Http/Controllers/Gateway/BitpayX.php
  34. 1 1
      app/Http/Controllers/Gateway/CodePay.php
  35. 2 4
      app/Http/Controllers/Gateway/F2Fpay.php
  36. 73 34
      app/Http/Controllers/NodeController.php
  37. 1 2
      app/Http/Controllers/User/AffiliateController.php
  38. 7 2
      app/Http/Controllers/User/SubscribeController.php
  39. 16 10
      app/Http/Controllers/UserController.php
  40. 2 2
      app/Http/Middleware/isForbidden.php
  41. 1 1
      app/Models/Article.php
  42. 1 1
      app/Models/Invite.php
  43. 1 1
      app/Models/Marketing.php
  44. 1 1
      app/Models/NodeCertificate.php
  45. 2 2
      app/Models/NotificationLog.php
  46. 1 1
      app/Models/Order.php
  47. 1 1
      app/Models/Payment.php
  48. 1 1
      app/Models/PaymentCallback.php
  49. 1 1
      app/Models/RuleGroup.php
  50. 19 4
      app/Models/SsNode.php
  51. 2 2
      app/Models/TicketReply.php
  52. 31 0
      app/Models/User.php
  53. 25 0
      app/Models/UserGroup.php
  54. 1 1
      app/Models/UserTrafficLog.php
  55. 3 4
      app/helpers.php
  56. 1 0
      config/app.php
  57. 0 0
      public/assets/custom/maps/jquery-jvectormap-world-mill-cn.js
  58. 0 0
      public/assets/custom/maps/jquery-jvectormap-world-mill-en.js
  59. 42 37
      public/assets/global/vendor/jvectormap/jquery-jvectormap.css
  60. 0 44
      public/assets/global/vendor/jvectormap/jquery-jvectormap.js
  61. 0 0
      public/assets/global/vendor/jvectormap/jquery-jvectormap.min.css
  62. 0 0
      public/assets/global/vendor/jvectormap/jquery-jvectormap.min.js
  63. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-au-mill-en.js
  64. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-ca-lcc-en.js
  65. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-de-mill-en.js
  66. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-europe-mill-en.js
  67. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-fr-mill-en.js
  68. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-uk_regions-mill-en.js
  69. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-us-merc-en.js
  70. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-us-ny-newyork-mill-en.js
  71. 0 0
      public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-za-mill-en.js
  72. 2 2
      readme.md
  73. 5 5
      resources/views/admin/config/config.blade.php
  74. 16 16
      resources/views/admin/config/system.blade.php
  75. 124 0
      resources/views/admin/group/groupInfo.blade.php
  76. 94 0
      resources/views/admin/group/groupList.blade.php
  77. 6 1
      resources/views/admin/layouts.blade.php
  78. 9 9
      resources/views/admin/node/nodeInfo.blade.php
  79. 34 7
      resources/views/admin/node/nodeList.blade.php
  80. 11 9
      resources/views/admin/shop/goodsInfo.blade.php
  81. 3 3
      resources/views/admin/tools/convert.blade.php
  82. 18 5
      resources/views/admin/user/userInfo.blade.php
  83. 2 2
      resources/views/components/avatar.blade.php
  84. 2 2
      resources/views/user/index.blade.php
  85. 100 4
      resources/views/user/nodeList.blade.php
  86. 8 0
      routes/web.php
  87. 16 3
      sql/db.sql
  88. 13 0
      sql/mod/20200725.sql

+ 4 - 9
app/Components/Helpers.php

@@ -102,12 +102,9 @@ class Helpers {
 
 	// 获取系统配置
 	public static function systemConfig(): array {
-		$config = Config::query()->get();
-		$data = [];
-		foreach($config as $vo){
+		foreach(Config::all() as $vo){
 			$data[$vo->name] = $vo->value;
 		}
-
 		$data['is_onlinePay'] = ($data['is_AliPay'] || $data['is_QQPay'] || $data['is_WeChatPay'] || $data['is_otherPay'])?: 0;
 
 		return $data;
@@ -115,11 +112,10 @@ class Helpers {
 
 	// 获取一个随机端口
 	public static function getRandPort() {
-		$config = self::systemConfig();
-		$port = random_int($config['min_port'], $config['max_port']);
+		$port = random_int(self::systemConfig()['min_port'], self::systemConfig()['max_port']);
 
 		$exists_port = User::query()->pluck('port')->toArray();
-		if(in_array($port, $exists_port, true) || in_array($port, self::$denyPorts)){
+		if(in_array($port, $exists_port, true) || in_array($port, self::$denyPorts, true)){
 			$port = self::getRandPort();
 		}
 
@@ -128,8 +124,7 @@ class Helpers {
 
 	// 获取一个随机端口
 	public static function getOnlyPort() {
-		$config = self::systemConfig();
-		$port = $config['min_port'];
+		$port = (int) self::systemConfig()['min_port'];
 
 		$exists_port = User::query()->where('port', '>=', $port)->pluck('port')->toArray();
 		while(in_array($port, $exists_port, true) || in_array($port, self::$denyPorts, true)){

+ 1 - 3
app/Components/IPIP.php

@@ -14,8 +14,6 @@ class IPIP {
 	 */
 	public static function ip($ip): ?array {
 		$filePath = database_path('ipip.ipdb');
-		$loc = new City($filePath);
-
-		return $loc->findMap($ip, 'CN');
+		return (new City($filePath))->findMap($ip, 'CN');
 	}
 }

+ 1 - 2
app/Components/Namesilo.php

@@ -32,8 +32,7 @@ class Namesilo {
 
 		$content = '请求操作:['.$operation.'] --- 请求数据:['.http_build_query($query).']';
 
-		$client = new Client(['timeout' => 10]);
-		$request = $client->get(self::$host.$operation.'?'.http_build_query($query));
+		$request = (new Client(['timeout' => 10]))->get(self::$host.$operation.'?'.http_build_query($query));
 		$result = XML2Array::createArray(json_decode($request->getBody(), true));
 
 		if($request->getStatusCode() != 200){

+ 3 - 5
app/Components/NetworkDetection.php

@@ -18,8 +18,7 @@ class NetworkDetection {
 	public static function networkCheck($ip, $type, $port = null) {
 		$url = 'https://api.50network.com/china-firewall/check/ip/'.($type? 'icmp/' : ($port? 'tcp_port/' : 'tcp_ack/')).$ip.($port? '/'.$port : '');
 		$checkName = $type? 'ICMP' : 'TCP';
-		$client = new Client(['timeout' => 10]);
-		$request = $client->get($url);
+		$request = (new Client(['timeout' => 10]))->get($url);
 		$result = json_decode($request->getBody(), true);
 
 		if($request->getStatusCode() == 200){
@@ -66,9 +65,8 @@ class NetworkDetection {
 	 */
 	public static function ping($ip) {
 		$url = 'https://api.oioweb.cn/api/hostping.php?host='.$ip;//https://api.iiwl.cc/api/ping.php?host=
-		$client = new Client(['timeout' => 20]);
-		$request = $client->get($url);
-		$message = json_decode($client->get($url)->getBody(), true);
+		$request = (new Client(['timeout' => 20]))->get($url);
+		$message = json_decode($request->getBody(), true);
 
 		// 发送成功
 		if($request->getStatusCode() == 200){

+ 2 - 4
app/Components/PushNotification.php

@@ -30,8 +30,7 @@ class PushNotification {
 	 */
 	private static function ServerChan($title, $content) {
 		// TODO:一天仅可发送不超过500条
-		$client = new Client(['timeout' => 5]);
-		$request = $client->get('https://sc.ftqq.com/'.Helpers::systemConfig()['server_chan_key'].'.send?text='.$title.'&desp='.urlencode($content));
+		$request = (new Client(['timeout' => 5]))->get('https://sc.ftqq.com/'.Helpers::systemConfig()['server_chan_key'].'.send?text='.$title.'&desp='.urlencode($content));
 		$message = json_decode($request->getBody(), true);
 		Log::debug($message);
 		// 发送成功
@@ -58,8 +57,7 @@ class PushNotification {
 	 * @return mixed
 	 */
 	private static function Bark($title, $content) {
-		$client = new Client(['timeout' => 5]);
-		$request = $client->get('https://api.day.app/'.Helpers::systemConfig()['bark_key'].'/'.$title.'/'.$content);
+		$request = (new Client(['timeout' => 5]))->get('https://api.day.app/'.Helpers::systemConfig()['bark_key'].'/'.$title.'/'.$content);
 		$message = json_decode($request->getBody(), true);
 
 		if($request->getStatusCode() == 200){

+ 9 - 14
app/Components/QQInfo.php

@@ -8,18 +8,15 @@ class QQInfo {
 	public static function getName(string $qq): string {
 		//向接口发起请求获取json数据
 		$url = 'https://r.qzone.qq.com/fcg-bin/cgi_get_portrait.fcg?get_nick=1&uins='.$qq;
-		$client = new Client(['timeout' => 10]);
-		$request = $client->get($url);
+		$request = (new Client(['timeout' => 10]))->get($url);
 		$message = mb_convert_encoding($request->getBody(), "UTF-8", "GBK");
 
 		// 接口是否异常
-		if($request->getStatusCode() == 200){
-			if(strpos($message, $qq) !== false){
-				//对获取的json数据进行截取并解析成数组
-				$message = json_decode(substr($message, 17, -1), true);
+		if($request->getStatusCode() == 200 && str_contains($message, $qq)){
+			//对获取的json数据进行截取并解析成数组
+			$message = json_decode(substr($message, 17, -1), true);
 
-				return stripslashes($message[$qq][6]);
-			}
+			return stripslashes($message[$qq][6]);
 		}
 
 		return $qq;
@@ -28,12 +25,11 @@ class QQInfo {
 	public static function getName2(string $qq): string {
 		//向接口发起请求获取json数据
 		$url = 'https://api.toubiec.cn/qq?qq='.$qq.'&size=100';
-		$client = new Client(['timeout' => 10]);
-		$request = $client->get($url);
+		$request = (new Client(['timeout' => 10]))->get($url);
 		$message = json_decode($request->getBody(), true);
 
 		// 接口是否异常
-		if($request->getStatusCode() == 200 && $message && $message['code'] == 200){
+		if($message && $message['code'] == 200 && $request->getStatusCode() == 200){
 			return $message['name'];
 		}
 
@@ -43,12 +39,11 @@ class QQInfo {
 	public static function getName3(string $qq): string {
 		//向接口发起请求获取json数据
 		$url = 'https://api.unipay.qq.com/v1/r/1450000186/wechat_query?cmd=1&pf=mds_storeopen_qb-__mds_qqclub_tab_-html5&pfkey=pfkey&from_h5=1&from_https=1&openid=openid&openkey=openkey&session_id=hy_gameid&session_type=st_dummy&qq_appid=&offerId=1450000186&sandbox=&provide_uin='.$qq;
-		$client = new Client(['timeout' => 10]);
-		$request = $client->get($url);
+		$request = (new Client(['timeout' => 10]))->get($url);
 		$message = json_decode($request->getBody(), true);
 
 		// 接口是否异常
-		if($request->getStatusCode() == 200 && $message && $message['ret'] == 0){
+		if($message && $message['ret'] == 0 && $request->getStatusCode() == 200){
 			return urldecode($message['nick']);
 		}
 

+ 2 - 4
app/Console/Commands/AutoJob.php

@@ -177,8 +177,7 @@ class AutoJob extends Command {
 	// 封禁访问异常的订阅链接
 	private function blockSubscribe(): void {
 		if(self::$systemConfig['is_subscribe_ban']){
-			$userList = User::query()->where('status', '>=', 0)->whereEnable(1)->get();
-			foreach($userList as $user){
+			foreach(User::query()->activeUser()->get() as $user){
 				$subscribe = UserSubscribe::query()->whereUserId($user->id)->first();
 				if($subscribe){
 					// 24小时内不同IP的请求次数
@@ -318,8 +317,7 @@ class AutoJob extends Command {
 	// 检测节点是否离线
 	private function checkNodeStatus(): void {
 		if(self::$systemConfig['is_node_offline']){
-			$nodeList = SsNode::whereIsRelay(0)->whereStatus(1)->get();
-			foreach($nodeList as $node){
+			foreach(SsNode::whereIsRelay(0)->whereStatus(1)->get() as $node){
 				// 10分钟内无节点负载信息则认为是后端炸了
 				$nodeTTL = SsNodeInfo::query()
 				                     ->whereNodeId($node->id)

+ 5 - 6
app/Console/Commands/AutoPingNode.php

@@ -15,8 +15,7 @@ class AutoPingNode extends Command {
 	public function handle(): void {
 		$jobStartTime = microtime(true);
 
-		$nodeList = SsNode::query()->whereIsRelay(0)->whereStatus(1)->get();
-		foreach($nodeList as $node){
+		foreach(SsNode::query()->whereIsRelay(0)->whereStatus(1)->get() as $node){
 			$this->pingNode($node->id, $node->is_ddns? $node->server : $node->ip);
 		}
 
@@ -33,10 +32,10 @@ class AutoPingNode extends Command {
 		if($result){
 			$obj = new SsNodePing();
 			$obj->node_id = $nodeId;
-			$obj->ct = intval($result['telecom']['time']);//电信
-			$obj->cu = intval($result['Unicom']['time']);// 联通
-			$obj->cm = intval($result['move']['time']);// 移动
-			$obj->hk = intval($result['HongKong']['time']);// 香港
+			$obj->ct = (int) $result['telecom']['time'];//电信
+			$obj->cu = (int) $result['Unicom']['time'];// 联通
+			$obj->cm = (int) $result['move']['time'];// 移动
+			$obj->hk = (int) $result['HongKong']['time'];// 香港
 			$obj->save();
 		}else{
 			Log::info("【".$ip."】Ping测速获取失败");

+ 1 - 2
app/Console/Commands/AutoStatisticsNodeDailyTraffic.php

@@ -15,8 +15,7 @@ class AutoStatisticsNodeDailyTraffic extends Command {
 	public function handle(): void {
 		$jobStartTime = microtime(true);
 
-		$nodeList = SsNode::query()->whereStatus(1)->orderBy('id')->get();
-		foreach($nodeList as $node){
+		foreach(SsNode::query()->whereStatus(1)->orderBy('id')->get() as $node){
 			$this->statisticsByNode($node->id);
 		}
 

+ 1 - 2
app/Console/Commands/AutoStatisticsNodeHourlyTraffic.php

@@ -15,8 +15,7 @@ class AutoStatisticsNodeHourlyTraffic extends Command {
 	public function handle(): void {
 		$jobStartTime = microtime(true);
 
-		$nodeList = SsNode::query()->whereStatus(1)->orderBy('id')->get();
-		foreach($nodeList as $node){
+		foreach(SsNode::query()->whereStatus(1)->orderBy('id')->get() as $node){
 			$this->statisticsByNode($node->id);
 		}
 

+ 2 - 4
app/Console/Commands/AutoStatisticsUserDailyTraffic.php

@@ -16,14 +16,12 @@ class AutoStatisticsUserDailyTraffic extends Command {
 	public function handle(): void {
 		$jobStartTime = microtime(true);
 
-		$userList = User::query()->where('status', '>=', 0)->whereEnable(1)->get();
-		foreach($userList as $user){
+		foreach(User::query()->activeUser()->get() as $user){
 			// 统计一次所有节点的总和
 			$this->statisticsByNode($user->id);
 
 			// 统计每个节点产生的流量
-			$nodeList = SsNode::query()->whereStatus(1)->orderBy('id')->get();
-			foreach($nodeList as $node){
+			foreach(SsNode::query()->whereStatus(1)->orderBy('id')->get() as $node){
 				$this->statisticsByNode($user->id, $node->id);
 			}
 		}

+ 2 - 4
app/Console/Commands/AutoStatisticsUserHourlyTraffic.php

@@ -16,14 +16,12 @@ class AutoStatisticsUserHourlyTraffic extends Command {
 	public function handle(): void {
 		$jobStartTime = microtime(true);
 
-		$userList = User::query()->where('status', '>=', 0)->whereEnable(1)->get();
-		foreach($userList as $user){
+		foreach(User::query()->activeUser()->get() as $user){
 			// 统计一次所有节点的总和
 			$this->statisticsByNode($user->id);
 
 			// 统计每个节点产生的流量
-			$nodeList = SsNode::query()->whereStatus(1)->orderBy('id')->get();
-			foreach($nodeList as $node){
+			foreach(SsNode::query()->whereStatus(1)->orderBy('id')->get() as $node){
 				$this->statisticsByNode($user->id, $node->id);
 			}
 		}

+ 1 - 2
app/Console/Commands/UserExpireAutoWarning.php

@@ -36,8 +36,7 @@ class UserExpireAutoWarning extends Command {
 
 	private function userExpireWarning(): void {
 		// 只取SSR没被禁用的用户,其他不用管
-		$userList = User::query()->whereEnable(1)->get();
-		foreach($userList as $user){
+		foreach(User::query()->whereEnable(1)->get() as $user){
 			// 用户名不是邮箱的跳过
 			if(false === filter_var($user->email, FILTER_VALIDATE_EMAIL)){
 				continue;

+ 1 - 2
app/Console/Commands/UserTrafficAutoWarning.php

@@ -35,8 +35,7 @@ class UserTrafficAutoWarning extends Command {
 
 	// 用户流量超过警告阈值自动发邮件提醒
 	private function userTrafficWarning(): void {
-		$userList = User::query()->where('status', '>=', 0)->whereEnable(1)->where('transfer_enable', '>', 0)->get();
-		foreach($userList as $user){
+		foreach(User::query()->activeUser()->where('transfer_enable', '>', 0)->get() as $user){
 			// 用户名不是邮箱的跳过
 			if(false === filter_var($user->email, FILTER_VALIDATE_EMAIL)){
 				continue;

+ 3 - 6
app/Console/Commands/updateTicket.php

@@ -14,13 +14,10 @@ class updateTicket extends Command {
 	public function handle(): void {
 		Log::info('----------------------------【更新工单】开始----------------------------');
 		// 获取管理员
-		$adminList = User::query()->whereIsAdmin(1)->get();
-		foreach($adminList as $admin){
+		foreach(User::query()->whereIsAdmin(1)->get() as $admin){
 			Log::info('----------------------------【更新管理员'.$admin->id.'回复工单】开始----------------------------');
-			// 获取该管理回复过的工单
-			$replyList = TicketReply::query()->whereUserId($admin->id)->get();
-			// 更新工单
-			foreach($replyList as $reply){
+			// 获取该管理回复过的工单, 更新工单
+			foreach(TicketReply::query()->whereUserId($admin->id)->get() as $reply){
 				$ret = TicketReply::query()->whereId($reply->id)->update(['user_id' => 0, 'admin_id' => $admin->id]);
 				if($ret){
 					Log::info('--- 管理员:'.$admin->email.'回复子单ID:'.$reply->id.' ---');

+ 1 - 2
app/Console/Commands/updateUserLevel.php

@@ -15,8 +15,7 @@ class updateUserLevel extends Command {
 	public function handle(): void {
 		Log::info('----------------------------【用户等级升级】开始----------------------------');
 		// 预设level 0
-		$users = User::query()->where('level', '<>', 0)->get();
-		foreach($users as $user){
+		foreach(User::query()->where('level', '<>', 0)->get() as $user){
 			User::query()->whereId($user->id)->update(['level' => 0]);
 		}
 		// 获取商品列表,取新等级

+ 5 - 5
app/Console/Commands/updateUserName.php

@@ -14,7 +14,7 @@ class updateUserName extends Command {
 	public function handle(): void {
 		Log::info('----------------------------【升级用户昵称】开始----------------------------');
 
-		$userList = User::query()->get();
+		$userList = User::all();
 		foreach($userList as $user){
 			$name = process($user->id);
 			User::query()->whereId($user->id)->update(['username' => $name]);
@@ -44,17 +44,17 @@ function process($id) {
 	if($user->qq){
 		$name = QQInfo::getName3($user->qq);
 		// 检测用户注册是否为QQ邮箱
-	}elseif(stripos($user->email, '@qq') != false){
+	}elseif(stripos($user->email, '@qq') !== false){
 		// 分离QQ邮箱后缀
-		$email = explode('@', $user->email);
+		$email = explode('@', $user->email, 2);
 		if(is_numeric($email[0])){
 			$name = QQInfo::getName3($email[0]);
-		}elseif(strpos($email[0], '.') !== false){
+		}elseif(str_contains($email[0], '.')){
 			$temp = explode('.', $email[0]);
 			if(is_numeric($temp[1])){
 				$name = QQInfo::getName3($temp[1]);
 			}else{
-				print_r($user->email.PHP_EOL);
+				echo $user->email.PHP_EOL;
 			}
 		}
 	}

+ 1 - 2
app/Console/Commands/upgradeUserResetTime.php

@@ -13,8 +13,7 @@ class upgradeUserResetTime extends Command {
 	public function handle(): void {
 		Log::info('----------------------------【升级用户重置日期】开始----------------------------');
 
-		$userList = User::query()->get();
-		foreach($userList as $user){
+		foreach(User::all() as $user){
 			$reset_time = null;
 			if($user->traffic_reset_day){
 				$today = date('d');// 今天 日期

+ 120 - 0
app/Http/Controllers/Admin/GroupController.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Models\SsNode;
+use App\Models\User;
+use App\Models\UserGroup;
+use Exception;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Redirect;
+use Response;
+use Validator;
+
+class GroupController extends Controller {
+	public function userGroupList(Request $request): \Illuminate\Http\Response {
+		$view['list'] = UserGroup::query()->orderByDesc('id')->paginate(15)->appends($request->except('page'));
+		return Response::view('admin.group.groupList', $view);
+	}
+
+	// 添加用户分组
+	public function addUserGroup(Request $request) {
+		if($request->isMethod('POST')){
+			$validator = Validator::make($request->all(), [
+				'name'  => 'required',
+				'nodes' => 'required',
+			]);
+
+			if($validator->fails()){
+				return Redirect::back()->withInput()->withErrors($validator->errors());
+			}
+
+			$ret = UserGroup::query()->insert([
+				'name'  => $request->input('name'),
+				'nodes' => implode(',', $request->input('nodes'))
+			]);
+
+			if($ret){
+				return Redirect::back()->with('successMsg', '操作成功');
+			}
+			return Redirect::back()->withInput()->withErrors('操作失败');
+		}
+		$view['nodeList'] = SsNode::query()->whereStatus(1)->get();
+		return Response::view('admin.group.groupInfo', $view);
+	}
+
+	// 编辑用户分组
+	public function editUserGroup(Request $request) {
+		$id = $request->input('id');
+		if($request->isMethod('POST')){
+			$validator = Validator::make($request->all(), [
+				'id'   => 'required',
+				'name' => 'required',
+			]);
+
+			if($validator->fails()){
+				return Redirect::back()->withInput()->withErrors($validator->errors());
+			}
+			$name = $request->input('name');
+			$nodes = $request->input('nodes');
+			$userGroup = UserGroup::query()->find($id);
+			if(!$userGroup){
+				return Redirect::back()->withInput()->withErrors('未找到需要编辑的用户分组!');
+			}
+
+			$data = [];
+			if($userGroup->name != $name){
+				$data['name'] = $name;
+			}
+
+			if($nodes){
+				$nodeStr = implode(',', $nodes);
+				if($userGroup->nodes != $nodeStr){
+					$data['nodes'] = $nodeStr;
+				}elseif($data == []){
+					return Redirect::back()->with('successMsg', '检测为未修改,无变动!');
+				}
+			}elseif(isset($userGroup->nodes)){
+				$data['nodes'] = $nodes;
+			}
+			$ret = UserGroup::query()->whereId($id)->update($data);
+			if($ret){
+				return Redirect::back()->with('successMsg', '操作成功');
+			}
+			return Redirect::back()->withInput()->withErrors('操作失败');
+		}
+
+		$userGroup = UserGroup::query()->find($id);
+		if(!$userGroup){
+			return Redirect::back();
+		}
+		$view['nodeList'] = SsNode::query()->whereStatus(1)->get();
+
+		return view('admin.group.groupInfo', $view)->with(compact('userGroup'));
+	}
+
+	// 删除用户分组
+	public function delUserGroup(Request $request): JsonResponse {
+		$id = $request->input('id');
+		// 校验该分组下是否存在关联账号
+		$userCount = User::query()->whereGroupId($id)->count();
+		if($userCount){
+			return Response::json(['status' => 'fail', 'message' => '该分组下存在关联账号,请先取消关联!']);
+		}
+
+		$userGroup = UserGroup::query()->whereId($id)->first();
+		if(!$userGroup){
+			return Response::json(['status' => 'fail', 'message' => '删除失败,未找到用户分组']);
+		}
+
+		try{
+			UserGroup::query()->whereId($id)->delete();
+		}catch(Exception $e){
+			return Response::json(['status' => 'fail', 'message' => '删除失败,'.$e->getMessage()]);
+		}
+
+		return Response::json(['status' => 'success', 'message' => '清理成功']);
+	}
+}

+ 1 - 2
app/Http/Controllers/Admin/MarketingController.php

@@ -69,8 +69,7 @@ class MarketingController extends Controller {
 
 		DB::beginTransaction();
 		try{
-			$client = new Client();
-			$response = $client->request('GET', 'https://pushbear.ftqq.com/sub', [
+			$response = (new Client())->get('https://pushbear.ftqq.com/sub', [
 				'query' => [
 					'sendkey' => self::$systemConfig['push_bear_send_key'],
 					'text'    => $title,

+ 9 - 10
app/Http/Controllers/Admin/RuleController.php

@@ -81,8 +81,7 @@ class RuleController extends Controller {
 		try{
 			Rule::query()->whereId($id)->delete();
 
-			$RuleGroupList = RuleGroup::query()->get();
-			foreach($RuleGroupList as $RuleGroup){
+			foreach(RuleGroup::all() as $RuleGroup){
 				$rules = explode(',', $RuleGroup->rules);
 				if(in_array($id, $rules, true)){
 					$rules = implode(',', array_diff($rules, [$id]));
@@ -116,7 +115,7 @@ class RuleController extends Controller {
 
 			$obj = new RuleGroup();
 			$obj->name = $request->input('name');
-			$obj->type = intval($request->input('type'));
+			$obj->type = (int) $request->input('type');
 			$obj->rules = implode(',', $request->input('rules'));
 			$obj->save();
 
@@ -125,7 +124,7 @@ class RuleController extends Controller {
 			}
 			return Redirect::back()->withInput()->withErrors('操作失败');
 		}
-		$view['ruleList'] = Rule::query()->get();
+		$view['ruleList'] = Rule::all();
 		return Response::view('admin.rule.ruleGroupInfo', $view);
 	}
 
@@ -143,7 +142,7 @@ class RuleController extends Controller {
 				return Redirect::back()->withInput()->withErrors($validator->errors());
 			}
 			$name = $request->input('name');
-			$type = intval($request->input('type'));
+			$type = (int) $request->input('type');
 			$rules = $request->input('rules');
 			$ruleGroup = RuleGroup::query()->find($id);
 			if(!$ruleGroup){
@@ -161,7 +160,7 @@ class RuleController extends Controller {
 				$ruleStr = implode(',', $rules);
 				if($ruleGroup->rules != $ruleStr){
 					$data['rules'] = $ruleStr;
-				}else{
+				}elseif($data == []){
 					return Redirect::back()->with('successMsg', '检测为未修改,无变动!');
 				}
 			}elseif(isset($ruleGroup->rules)){
@@ -178,7 +177,7 @@ class RuleController extends Controller {
 		if(!$ruleGroup){
 			return Redirect::back();
 		}
-		$view['ruleList'] = Rule::query()->get();
+		$view['ruleList'] = Rule::all();
 
 		return view('admin.rule.ruleGroupInfo', $view)->with(compact('ruleGroup'));
 	}
@@ -247,7 +246,7 @@ class RuleController extends Controller {
 		}
 
 		$view['ruleGroup'] = RuleGroup::query()->find($id);
-		$view['nodeList'] = SsNode::query()->get();
+		$view['nodeList'] = SsNode::all();
 
 		return Response::view('admin.rule.assignNode', $view);
 	}
@@ -275,8 +274,8 @@ class RuleController extends Controller {
 			$query->whereRuleId($ruleId);
 		}
 
-		$view['nodeList'] = SsNode::query()->get();
-		$view['ruleList'] = Rule::query()->get();
+		$view['nodeList'] = SsNode::all();
+		$view['ruleList'] = Rule::all();
 		$view['ruleLogs'] = $query->paginate(15)->appends($request->except('page'));
 		return Response::view('admin.rule.ruleLogList', $view);
 	}

+ 2 - 2
app/Http/Controllers/Admin/ShopController.php

@@ -101,7 +101,7 @@ class ShopController extends Controller {
 				return Redirect::back()->withInput()->withErrors('添加失败');
 			}
 		}else{
-			$view['level_list'] = Level::query()->orderBy('level')->get();
+			$view['levelList'] = Level::query()->orderBy('level')->get();
 
 			return Response::view('admin.shop.goodsInfo', $view);
 		}
@@ -176,7 +176,7 @@ class ShopController extends Controller {
 		}
 
 		$goods = Goods::query()->whereId($id)->first();
-		$view['level_list'] = Level::query()->orderBy('level')->get();
+		$view['levelList'] = Level::query()->orderBy('level')->get();
 
 		return view('admin.shop.goodsInfo', $view)->with(compact('goods'));
 	}

+ 6 - 6
app/Http/Controllers/Admin/ToolsController.php

@@ -31,9 +31,9 @@ class ToolsController extends Controller {
 			foreach($content as $item){
 				// 判断是SS还是SSR链接
 				$str = '';
-				if(false !== strpos($item, 'ssr://')){
+				if(str_contains($item, 'ssr://')){
 					$str = mb_substr($item, 6);
-				}elseif(false !== strpos($item, 'ss://')){
+				}elseif(str_contains($item, 'ss://')){
 					$str = mb_substr($item, 5);
 				}
 
@@ -101,9 +101,9 @@ class ToolsController extends Controller {
 		}
 
 		// 加密方式、协议、混淆
-		$view['method_list'] = Helpers::methodList();
-		$view['protocol_list'] = Helpers::protocolList();
-		$view['obfs_list'] = Helpers::obfsList();
+		$view['methodList'] = Helpers::methodList();
+		$view['protocolList'] = Helpers::protocolList();
+		$view['obfsList'] = Helpers::obfsList();
 
 		return Response::view('admin.tools.convert', $view);
 	}
@@ -219,7 +219,7 @@ class ToolsController extends Controller {
 		}else{
 			$url = [];
 			foreach($logs as $log){
-				if(strpos($log, 'TCP connecting')){
+				if(str_contains($log, 'TCP connecting')){
 					continue;
 				}
 

+ 36 - 31
app/Http/Controllers/AdminController.php

@@ -24,6 +24,7 @@ use App\Models\SsNodeTrafficDaily;
 use App\Models\User;
 use App\Models\UserBanLog;
 use App\Models\UserCreditLog;
+use App\Models\UserGroup;
 use App\Models\UserLoginLog;
 use App\Models\UserSubscribe;
 use App\Models\UserTrafficDaily;
@@ -253,13 +254,14 @@ class AdminController extends Controller {
 			$user->method = $request->input('method');
 			$user->protocol = $request->input('protocol');
 			$user->obfs = $request->input('obfs');
-			$user->speed_limit = $request->input('speed_limit');
+			$user->speed_limit = $request->input('speed_limit') * Mbps;
 			$user->wechat = $request->input('wechat');
 			$user->qq = $request->input('qq');
 			$user->enable_time = $request->input('enable_time')?: date('Y-m-d');
 			$user->expire_time = $request->input('expire_time')?: date('Y-m-d', strtotime("+365 days"));
 			$user->remark = str_replace(["atob", "eval"], "", $request->input('remark'));
 			$user->level = $request->input('level')?: 0;
+			$user->group_id = $request->input('group_id')?: 0;
 			$user->reg_ip = getClientIp();
 			$user->reset_time = $request->input('reset_time') > date('Y-m-d')? $request->input('reset_time') : null;
 			$user->invite_num = $request->input('invite_num')?: 0;
@@ -285,10 +287,11 @@ class AdminController extends Controller {
 		}
 
 		// 生成一个可用端口
-		$view['method_list'] = Helpers::methodList();
-		$view['protocol_list'] = Helpers::protocolList();
-		$view['obfs_list'] = Helpers::obfsList();
-		$view['level_list'] = Level::query()->orderBy('level')->get();
+		$view['methodList'] = Helpers::methodList();
+		$view['protocolList'] = Helpers::protocolList();
+		$view['obfsList'] = Helpers::obfsList();
+		$view['levelList'] = Level::query()->orderBy('level')->get();
+		$view['groupList'] = UserGroup::query()->orderBy('id')->get();
 
 		return Response::view('admin.user.userInfo', $view);
 	}
@@ -374,24 +377,25 @@ class AdminController extends Controller {
 					'method'          => $request->input('method'),
 					'protocol'        => $request->input('protocol'),
 					'obfs'            => $request->input('obfs'),
-					'speed_limit'     => $request->input('speed_limit'),
+					'speed_limit'     => $request->input('speed_limit') * Mbps,
 					'wechat'          => $request->input('wechat'),
 					'qq'              => $request->input('qq'),
 					'enable_time'     => $request->input('enable_time')?: date('Y-m-d'),
 					'expire_time'     => $request->input('expire_time')?: date('Y-m-d', strtotime("+365 days")),
 					'remark'          => str_replace("eval", "", str_replace("atob", "", $request->input('remark'))),
 					'level'           => $request->input('level'),
+					'group_id'        => $request->input('group_id'),
 					'reset_time'      => $request->input('reset_time'),
 					'status'          => $status
 				];
 
 				// 只有admin才有权限操作管理员属性
 				if(Auth::getUser()->is_admin == 1){
-					$data['is_admin'] = intval($is_admin);
+					$data['is_admin'] = (int) $is_admin;
 				}
 
 				// 非演示环境才可以修改管理员密码
-				if(!empty($password) && !(env('APP_DEMO') && $id == 1)){
+				if(!empty($password) && !(config('app.demo') && $id == 1)){
 					$data['password'] = Hash::make($password);
 				}
 
@@ -419,10 +423,11 @@ class AdminController extends Controller {
 			}
 
 			$view['user'] = $user;
-			$view['method_list'] = Helpers::methodList();
-			$view['protocol_list'] = Helpers::protocolList();
-			$view['obfs_list'] = Helpers::obfsList();
-			$view['level_list'] = Level::query()->orderBy('level')->get();
+			$view['methodList'] = Helpers::methodList();
+			$view['protocolList'] = Helpers::protocolList();
+			$view['obfsList'] = Helpers::obfsList();
+			$view['levelList'] = Level::query()->orderBy('level')->get();
+			$view['groupList'] = UserGroup::query()->orderBy('id')->get();
 
 			return view('admin.user.userInfo', $view)->with(compact('user'));
 		}
@@ -695,7 +700,7 @@ class AdminController extends Controller {
 		$filePath = public_path('downloads/'.$fileName);
 		file_put_contents($filePath, $json);
 
-		if(!file_exists($filePath)){
+		if(!is_file($filePath)){
 			exit('文件生成失败,请检查目录权限');
 		}
 
@@ -824,16 +829,16 @@ class AdminController extends Controller {
 			return Response::json(['status' => 'success', 'data' => '', 'message' => '添加成功']);
 		}
 
-		$labelList = Label::query()->get();
+		$labelList = Label::all();
 		foreach($labelList as $label){
 			$label->nodeCount = SsNodeLabel::query()->whereLabelId($label->id)->groupBy('label_id')->count();
 		}
 
-		$view['method_list'] = SsConfig::type(1)->get();
-		$view['protocol_list'] = SsConfig::type(2)->get();
-		$view['obfs_list'] = SsConfig::type(3)->get();
-		$view['country_list'] = Country::query()->get();
-		$view['level_list'] = Level::query()->get();
+		$view['methodList'] = SsConfig::type(1)->get();
+		$view['protocolList'] = SsConfig::type(2)->get();
+		$view['obfsList'] = SsConfig::type(3)->get();
+		$view['countryList'] = Country::all();
+		$view['levelList'] = Level::all();
 		$view['labelList'] = $labelList;
 
 		return Response::view('admin.config.config', $view);
@@ -1105,14 +1110,14 @@ class AdminController extends Controller {
 	// 系统设置
 	public function system(): \Illuminate\Http\Response {
 		$view = self::$systemConfig;
-		$view['label_list'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
+		$view['labelList'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
 
 		return Response::view('admin.config.system', $view);
 	}
 
 	// 设置某个配置项
 	public function setConfig(Request $request): JsonResponse {
-		$name = $request->input('name');
+		$name = (string) $request->input('name');
 		$value = $request->input('value');
 
 		if(!$name){
@@ -1125,21 +1130,21 @@ class AdminController extends Controller {
 		}
 
 		// 如果开启用户邮件重置密码,则先设置网站名称和网址
-		if($value != '0'
-		   && in_array($name, ['is_reset_password', 'is_activate_account', 'expire_warning', 'traffic_warning'])){
-			$config = Config::query()->whereName('website_name')->first();
-			if($config->value == ''){
+		if($value !== '0'
+		   && in_array($name, ['is_reset_password', 'is_activate_account', 'expire_warning', 'traffic_warning'], true)){
+			$config = Config::query()->whereName('website_name')->firstOrFail();
+			if(!$config->value){
 				return Response::json(['status' => 'fail', 'message' => '设置失败:启用该配置需要先设置【网站名称】']);
 			}
 
-			$config = Config::query()->whereName('website_url')->first();
-			if($config->value == ''){
+			$config = Config::query()->whereName('website_url')->firstOrFail();
+			if(!$config->value){
 				return Response::json(['status' => 'fail', 'message' => '设置失败:启用该配置需要先设置【网站地址】']);
 			}
 		}
 
 		// 支付设置判断
-		if($value != '' && in_array($name, ['is_AliPay', 'is_QQPay', 'is_WeChatPay', 'is_otherPay'])){
+		if($value !== '' && in_array($name, ['is_AliPay', 'is_QQPay', 'is_WeChatPay', 'is_otherPay'], true)){
 			switch($value){
 				case 'f2fpay':
 					if(!self::$systemConfig['f2fpay_app_id'] || !self::$systemConfig['f2fpay_private_key']
@@ -1181,7 +1186,7 @@ class AdminController extends Controller {
 		}
 
 		// 演示环境禁止修改特定配置项
-		if(env('APP_DEMO')){
+		if(config('app.demo')){
 			$denyConfig = [
 				'website_url',
 				'min_rand_traffic',
@@ -1199,7 +1204,7 @@ class AdminController extends Controller {
 
 		// 如果是返利比例,则需要除100
 		if($name === 'referral_percent'){
-			$value = intval($value) / 100;
+			$value = (int) $value / 100;
 		}
 
 		// 更新配置
@@ -1469,7 +1474,7 @@ class AdminController extends Controller {
 		$wechat = $request->input('wechat');
 		$qq = $request->input('qq');
 
-		$query = User::query()->where('status', '>=', 0)->whereEnable(1);
+		$query = User::query()->activeUser();
 
 		if(isset($email)){
 			$query->where('email', 'like', '%'.$email.'%');

+ 7 - 2
app/Http/Controllers/Api/LoginController.php

@@ -77,8 +77,13 @@ class LoginController extends Controller {
 			$url = self::$systemConfig['subscribe_domain']?: self::$systemConfig['website_url'];
 
 			// 节点列表
-			$nodeList = SsNode::query()->whereStatus(1)->where('level', '<=', $user->level)->get();
-
+			$nodeList = SsNode::query()
+			                  ->whereStatus(1)
+			                  ->GroupNodePermit($user->group_id)
+			                  ->where('level', '<=', $user->level)
+			                  ->orderByDesc('sort')
+			                  ->orderBy('id')
+			                  ->get();
 
 			$c_nodes = collect();
 			foreach($nodeList as $node){

+ 10 - 13
app/Http/Controllers/Api/WebApi/BaseController.php

@@ -19,9 +19,9 @@ use Response;
 class BaseController {
 	// 上报节点心跳信息
 	public function setNodeStatus(Request $request, $id): JsonResponse {
-		$cpu = intval($request->input('cpu')) / 100;
-		$mem = intval($request->input('mem')) / 100;
-		$disk = intval($request->input('disk')) / 100;
+		$cpu = (int) $request->input('cpu') / 100;
+		$mem = (int) $request->input('mem') / 100;
+		$disk = (int) $request->input('disk') / 100;
 
 		if(is_null($request->input('uptime'))){
 			return $this->returnData('上报节点心跳信息失败,请检查字段');
@@ -29,7 +29,7 @@ class BaseController {
 
 		$obj = new SsNodeInfo();
 		$obj->node_id = $id;
-		$obj->uptime = intval($request->input('uptime'));
+		$obj->uptime = (int) $request->input('uptime');
 		//$obj->load = $request->input('load');
 		$obj->load = implode(' ', [$cpu, $mem, $disk]);
 		$obj->log_time = time();
@@ -43,7 +43,7 @@ class BaseController {
 	}
 
 	// 返回数据
-	public function returnData($message, $status = 'fail', $code = 400, $data = '', $addition = false): JsonResponse {
+	public function returnData($message, $status = 'fail', $code = 400, $data = '', $addition = []): JsonResponse {
 		$data = ['status' => $status, 'code' => $code, 'data' => $data, 'message' => $message];
 
 		if($addition){
@@ -95,9 +95,7 @@ class BaseController {
 
 	// 上报用户流量日志
 	public function setUserTraffic(Request $request, $id): JsonResponse {
-		$inputArray = $request->all();
-
-		foreach($inputArray as $input){
+		foreach($request->all() as $input){
 			if(!array_key_exists('uid', $input)){
 				return $this->returnData('上报用户流量日志失败,请检查字段');
 			}
@@ -105,9 +103,9 @@ class BaseController {
 			$rate = SsNode::find($id)->traffic_rate;
 
 			$obj = new UserTrafficLog();
-			$obj->user_id = intval($input['uid']);
-			$obj->u = intval($input['upload']) * $rate;
-			$obj->d = intval($input['download']) * $rate;
+			$obj->user_id = (int) $input['uid'];
+			$obj->u = (int) $input['upload'] * $rate;
+			$obj->d = (int) $input['download'] * $rate;
 			$obj->node_id = $id;
 			$obj->rate = $rate;
 			$obj->traffic = flowAutoShow($obj->u + $obj->d);
@@ -130,8 +128,7 @@ class BaseController {
 		if($nodeRule){
 			$ruleGroup = RuleGroup::query()->whereId($nodeRule->rule_group_id)->first();
 			if($ruleGroup){
-				$rules = explode(',', $ruleGroup->rules);
-				foreach($rules as $ruleId){
+				foreach(explode(',', $ruleGroup->rules) as $ruleId){
 					$rule = Rule::query()->whereId($ruleId)->first();
 					if($rule){
 						$new = [

+ 6 - 1
app/Http/Controllers/Api/WebApi/TrojanController.php

@@ -28,7 +28,12 @@ class TrojanController extends BaseController {
 	// 获取节点可用的用户列表
 	public function getUserList($id): JsonResponse {
 		$node = SsNode::query()->whereId($id)->first();
-		$users = User::query()->where('status', '<>', -1)->whereEnable(1)->where('level', '>=', $node->level)->get();
+		$users = User::query()
+		             ->where('status', '<>', -1)
+		             ->whereEnable(1)
+		             ->groupUserPermit($node->id)
+		             ->where('level', '>=', $node->level)
+		             ->get();
 		$data = [];
 
 		foreach($users as $user){

+ 6 - 1
app/Http/Controllers/Api/WebApi/V2RayController.php

@@ -41,7 +41,12 @@ class V2RayController extends BaseController {
 	// 获取节点可用的用户列表
 	public function getUserList($id): JsonResponse {
 		$node = SsNode::query()->whereId($id)->first();
-		$users = User::query()->where('status', '<>', -1)->whereEnable(1)->where('level', '>=', $node->level)->get();
+		$users = User::query()
+		             ->where('status', '<>', -1)
+		             ->whereEnable(1)
+		             ->groupUserPermit($node->id)
+		             ->where('level', '>=', $node->level)
+		             ->get();
 		$data = [];
 
 		foreach($users as $user){

+ 7 - 2
app/Http/Controllers/Api/WebApi/VNetController.php

@@ -22,7 +22,7 @@ class VNetController extends BaseController {
 			'speed_limit'  => $node->speed_limit,
 			'client_limit' => $node->client_limit,
 			'single'       => $node->single,
-			'port'         => strval($node->port),
+			'port'         => (string) $node->port,
 			'passwd'       => $node->passwd?: '',
 			'push_port'    => $node->push_port,
 			'secret'       => $node->auth->secret,
@@ -33,7 +33,12 @@ class VNetController extends BaseController {
 	// 获取节点可用的用户列表
 	public function getUserList($id): JsonResponse {
 		$node = SsNode::query()->whereId($id)->first();
-		$users = User::query()->where('status', '<>', -1)->whereEnable(1)->where('level', '>=', $node->level)->get();
+		$users = User::query()
+		             ->where('status', '<>', -1)
+		             ->whereEnable(1)
+		             ->groupUserPermit($node->id)
+		             ->where('level', '>=', $node->level)
+		             ->get();
 		$data = [];
 
 		foreach($users as $user){

+ 16 - 7
app/Http/Controllers/AuthController.php

@@ -166,7 +166,7 @@ class AuthController extends Controller {
 	private function addUserLoginLog($userId, $ip): void {
 		if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
 			Log::info('识别到IPv6,尝试解析:'.$ip);
-			$ipInfo = getIPv6($ip);
+			$ipInfo = getIPInfo($ip);
 		}else{
 			$ipInfo = QQWry::ip($ip); // 通过纯真IP库解析IPv4信息
 			if(isset($ipInfo['error'])){
@@ -242,10 +242,10 @@ class AuthController extends Controller {
 			$register_token = $request->input('register_token');
 			$code = $request->input('code');
 			$verify_code = $request->input('verify_code');
-			$aff = intval($request->input('aff'));
+			$aff = (int) $request->input('aff');
 
 			// 防止重复提交
-			if($register_token != Session::get('register_token')){
+			if($register_token !== Session::get('register_token')){
 				return Redirect::back()->withInput()->withErrors(trans('auth.repeat_request'));
 			}
 
@@ -258,8 +258,8 @@ class AuthController extends Controller {
 
 			// 校验域名邮箱黑白名单
 			if(self::$systemConfig['is_email_filtering']){
-				$result = $this->emailChecker($email);
-				if($result != false){
+				$result = $this->emailChecker($email, 1);
+				if($result !== false){
 					return $result;
 				}
 			}
@@ -406,23 +406,32 @@ class AuthController extends Controller {
 	}
 
 	//邮箱检查
-	private function emailChecker($email) {
+	private function emailChecker($email, $returnType = 0) {
 		$sensitiveWords = $this->sensitiveWords(self::$systemConfig['is_email_filtering']);
 		$emailSuffix = explode('@', $email); // 提取邮箱后缀
 		switch(self::$systemConfig['is_email_filtering']){
 			// 黑名单
 			case 1:
 				if(in_array(strtolower($emailSuffix[1]), $sensitiveWords, true)){
+					if($returnType){
+						return Redirect::back()->withErrors(trans('auth.email_banned'));
+					}
 					return Response::json(['status' => 'fail', 'message' => trans('auth.email_banned')]);
 				}
 				break;
 			//白名单
 			case 2:
 				if(!in_array(strtolower($emailSuffix[1]), $sensitiveWords, true)){
+					if($returnType){
+						return Redirect::back()->withErrors(trans('auth.email_invalid'));
+					}
 					return Response::json(['status' => 'fail', 'message' => trans('auth.email_invalid')]);
 				}
 				break;
 			default:
+				if($returnType){
+					return Redirect::back()->withErrors(trans('auth.email_invalid'));
+				}
 				return Response::json(['status' => 'fail', 'message' => trans('auth.email_invalid')]);
 		}
 
@@ -743,7 +752,7 @@ class AuthController extends Controller {
 		// 校验域名邮箱黑白名单
 		if(self::$systemConfig['is_email_filtering']){
 			$result = $this->emailChecker($email);
-			if($result != false){
+			if($result !== false){
 				return $result;
 			}
 		}

+ 2 - 1
app/Http/Controllers/Controller.php

@@ -11,6 +11,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Routing\Controller as BaseController;
+use Ramsey\Uuid\UuidInterface;
 use RuntimeException;
 use Str;
 
@@ -23,7 +24,7 @@ class Controller extends BaseController {
 	}
 
 	// 生成UUID
-	public function makeUUID() {
+	public function makeUUID(): UuidInterface {
 		return Str::uuid();
 	}
 

+ 1 - 2
app/Http/Controllers/Gateway/AbstractPayment.php

@@ -12,7 +12,6 @@ use App\Models\User;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Log;
-use Str;
 
 abstract class AbstractPayment {
 	protected static $systemConfig;
@@ -39,7 +38,7 @@ abstract class AbstractPayment {
 		$user = User::find($order->user_id);
 
 		//余额充值
-		if($order->goods_id == 0 || $order->goods_id == null){
+		if($order->goods_id == 0){
 			Order::query()->whereOid($order->oid)->update(['status' => 2]);
 			User::query()->whereId($order->user_id)->increment('credit', $order->amount * 100);
 			// 余额变动记录日志

+ 5 - 4
app/Http/Controllers/Gateway/BitpayX.php

@@ -7,6 +7,7 @@ use Auth;
 use GuzzleHttp\Client;
 use Illuminate\Http\JsonResponse;
 use Log;
+use Psr\Http\Message\StreamInterface;
 use Response;
 
 class BitpayX extends AbstractPayment {
@@ -52,12 +53,12 @@ class BitpayX extends AbstractPayment {
 			'secret'            => parent::$systemConfig['bitpay_secret'],
 			'type'              => 'FIAT'
 		];
-		ksort($data_sign);
+		ksort($data_sign, SORT_STRING);
 
 		return http_build_query($data_sign);
 	}
 
-	private function mprequest($data, $type = 'pay') {
+	private function mprequest($data, $type = 'pay'): StreamInterface {
 		$client = new Client(['base_uri' => 'https://api.mugglepay.com/v1/', 'timeout' => 10]);
 
 		if($type === 'query'){
@@ -91,9 +92,9 @@ class BitpayX extends AbstractPayment {
 		}
 		// 准备待签名数据
 		$str_to_sign = $this->prepareSignId($inputJSON['merchant_order_id']);
-		$isPaid = $data != null && $data['status'] != null && $data['status'] === 'PAID';
+		$isPaid = $data != null && $data['status'] && $data['status'] === 'PAID';
 
-		if($this->sign($str_to_sign) === $inputJSON['token'] && $isPaid){
+		if($isPaid && $this->sign($str_to_sign) === $inputJSON['token']){
 			$this->postPayment($inputJSON['merchant_order_id'], 'BitPayX');
 			echo json_encode(['status' => 200]);
 		}else{

+ 1 - 1
app/Http/Controllers/Gateway/CodePay.php

@@ -60,7 +60,7 @@ class CodePay extends AbstractPayment {
 			}
 			$sign .= "$key=$val";
 		}
-		if(!$_POST['pay_no'] || md5($sign.parent::$systemConfig['codepay_key']) != $_POST['sign']){
+		if(!$_POST['pay_no'] || hash_equals($sign.parent::$systemConfig['codepay_key'], $_POST['sign'])){
 			exit('fail');
 		}
 		$payment = Payment::whereTradeNo($_POST['pay_id'])->first();

+ 2 - 4
app/Http/Controllers/Gateway/F2Fpay.php

@@ -42,8 +42,7 @@ class F2Fpay extends AbstractPayment {
 		];
 
 		try{
-			$client = new Client(Client::ALIPAY, self::$aliConfig);
-			$result = $client->pay(Client::ALI_CHANNEL_QR, $data);
+			$result = (new Client(Client::ALIPAY, self::$aliConfig))->pay(Client::ALI_CHANNEL_QR, $data);
 		}catch(InvalidArgumentException $e){
 			Log::error("【支付宝当面付】输入信息错误: ".$e->getMessage());
 			exit;
@@ -67,8 +66,7 @@ class F2Fpay extends AbstractPayment {
 		];
 
 		try{
-			$client = new Client(Client::ALIPAY, self::$aliConfig);
-			$result = $client->tradeQuery($data);
+			$result = (new Client(Client::ALIPAY, self::$aliConfig))->tradeQuery($data);
 			Log::info("【支付宝当面付】回调验证查询:".var_export($result, true));
 		}catch(InvalidArgumentException $e){
 			Log::error("【支付宝当面付】回调信息错误: ".$e->getMessage());

+ 73 - 34
app/Http/Controllers/NodeController.php

@@ -18,6 +18,7 @@ use App\Models\SsNodeOnlineLog;
 use App\Models\SsNodePing;
 use App\Models\SsNodeTrafficDaily;
 use App\Models\SsNodeTrafficHourly;
+use App\Models\UserGroup;
 use App\Models\UserTrafficDaily;
 use App\Models\UserTrafficHourly;
 use App\Models\UserTrafficLog;
@@ -112,7 +113,7 @@ class NodeController extends Controller {
 				$node->relay_server = $request->input('relay_server');
 				$node->relay_port = $request->input('relay_port');
 				$node->level = $request->input('level');
-				$node->speed_limit = intval($request->input('speed_limit')) * Mbps;
+				$node->speed_limit = (int) $request->input('speed_limit') * Mbps;
 				$node->client_limit = $request->input('client_limit');
 				$node->description = $request->input('description');
 				$node->method = $request->input('method');
@@ -121,18 +122,18 @@ class NodeController extends Controller {
 				$node->obfs = $request->input('obfs');
 				$node->obfs_param = $request->input('obfs_param');
 				$node->traffic_rate = $request->input('traffic_rate');
-				$node->is_subscribe = intval($request->input('is_subscribe'));
-				$node->is_ddns = intval($request->input('is_ddns'));
-				$node->is_relay = intval($request->input('is_relay'));
-				$node->is_udp = intval($request->input('is_udp'));
+				$node->is_subscribe = (int) $request->input('is_subscribe');
+				$node->is_ddns = (int) $request->input('is_ddns');
+				$node->is_relay = (int) $request->input('is_relay');
+				$node->is_udp = (int) $request->input('is_udp');
 				$node->push_port = $request->input('push_port');
 				$node->detection_type = $request->input('detection_type');
-				$node->compatible = intval($request->input('compatible'));
-				$node->single = intval($request->input('single'));
+				$node->compatible = (int) $request->input('compatible');
+				$node->single = (int) $request->input('single');
 				$node->port = $request->input('port');
 				$node->passwd = $request->input('passwd');
 				$node->sort = $request->input('sort');
-				$node->status = intval($request->input('status'));
+				$node->status = (int) $request->input('status');
 				$node->v2_alter_id = $request->input('v2_alter_id');
 				$node->v2_port = $request->input('v2_port');
 				$node->v2_method = $request->input('v2_method');
@@ -140,13 +141,14 @@ class NodeController extends Controller {
 				$node->v2_type = $request->input('v2_type');
 				$node->v2_host = $request->input('v2_host')?: '';
 				$node->v2_path = $request->input('v2_path');
-				$node->v2_tls = intval($request->input('v2_tls'));
+				$node->v2_tls = (int) $request->input('v2_tls');
 				$node->tls_provider = $request->input('tls_provider');
 				$node->save();
 
 				DB::commit();
 				// 生成节点标签
 				$this->makeLabels($node->id, $request->input('labels'));
+				$this->getNodeGeo($node->id);
 
 				return Response::json(['status' => 'success', 'message' => '添加成功']);
 			}catch(Exception $e){
@@ -156,13 +158,13 @@ class NodeController extends Controller {
 				return Response::json(['status' => 'fail', 'message' => '添加失败:'.$e->getMessage()]);
 			}
 		}else{
-			$view['method_list'] = Helpers::methodList();
-			$view['protocol_list'] = Helpers::protocolList();
-			$view['obfs_list'] = Helpers::obfsList();
-			$view['country_list'] = Country::query()->orderBy('code')->get();
-			$view['level_list'] = Level::query()->orderBy('level')->get();
-			$view['label_list'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
-			$view['dv_list'] = NodeCertificate::query()->orderBy('id')->get();
+			$view['methodList'] = Helpers::methodList();
+			$view['protocolList'] = Helpers::protocolList();
+			$view['obfsList'] = Helpers::obfsList();
+			$view['countryList'] = Country::query()->orderBy('code')->get();
+			$view['levelList'] = Level::query()->orderBy('level')->get();
+			$view['labelList'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
+			$view['dvList'] = NodeCertificate::query()->orderBy('id')->get();
 
 			return Response::view('admin.node.nodeInfo', $view);
 		}
@@ -238,6 +240,36 @@ class NodeController extends Controller {
 		}
 	}
 
+	// 获取节点地理位置
+	private function getNodeGeo($id): bool {
+		$nodes = SsNode::query()->whereStatus(1);
+		if($id){
+			$nodes = $nodes->whereId($id)->get();
+		}else{
+			$nodes = $nodes->get();
+		}
+		$result = 0;
+		foreach($nodes as $node){
+			$data = getIPInfo($node->is_ddns == 1? gethostbyname($node->server) : $node->ip);
+			if($data){
+				$ret = SsNode::query()->whereId($node->id)->update(['geo' => $data['latitude'].','.$data['longitude']]);
+				if($ret){
+					$result++;
+				}
+			}
+		}
+		return $result;
+	}
+
+	// 刷新节点地理位置
+	public function refreshGeo(Request $request): JsonResponse {
+		if($this->getNodeGeo($request->input('id', 0))){
+			return Response::json(['status' => 'success', 'message' => '获取地理位置更新成功!']);
+		}
+		return Response::json(['status' => 'fail', 'message' => '获取地理位置更新失败!']);
+	}
+
+
 	// 编辑节点
 	public function editNode(Request $request) {
 		$id = $request->input('id');
@@ -261,7 +293,7 @@ class NodeController extends Controller {
 					'relay_server'   => $request->input('relay_server'),
 					'relay_port'     => $request->input('relay_port'),
 					'level'          => $request->input('level'),
-					'speed_limit'    => intval($request->input('speed_limit')) * Mbps,
+					'speed_limit'    => (int) $request->input('speed_limit') * Mbps,
 					'client_limit'   => $request->input('client_limit'),
 					'description'    => $request->input('description'),
 					'method'         => $request->input('method'),
@@ -270,18 +302,18 @@ class NodeController extends Controller {
 					'obfs'           => $request->input('obfs'),
 					'obfs_param'     => $request->input('obfs_param'),
 					'traffic_rate'   => $request->input('traffic_rate'),
-					'is_subscribe'   => intval($request->input('is_subscribe')),
-					'is_ddns'        => intval($request->input('is_ddns')),
-					'is_relay'       => intval($request->input('is_relay')),
-					'is_udp'         => intval($request->input('is_udp')),
+					'is_subscribe'   => (int) $request->input('is_subscribe'),
+					'is_ddns'        => (int) $request->input('is_ddns'),
+					'is_relay'       => (int) $request->input('is_relay'),
+					'is_udp'         => (int) $request->input('is_udp'),
 					'push_port'      => $request->input('push_port'),
 					'detection_type' => $request->input('detection_type'),
-					'compatible'     => intval($request->input('compatible')),
-					'single'         => intval($request->input('single')),
+					'compatible'     => (int) $request->input('compatible'),
+					'single'         => (int) $request->input('single'),
 					'port'           => $request->input('port'),
 					'passwd'         => $request->input('passwd'),
 					'sort'           => $request->input('sort'),
-					'status'         => intval($request->input('status')),
+					'status'         => (int) $request->input('status'),
 					'v2_alter_id'    => $request->input('v2_alter_id'),
 					'v2_port'        => $request->input('v2_port'),
 					'v2_method'      => $request->input('v2_method'),
@@ -289,7 +321,7 @@ class NodeController extends Controller {
 					'v2_type'        => $request->input('v2_type'),
 					'v2_host'        => $request->input('v2_host')?: '',
 					'v2_path'        => $request->input('v2_path'),
-					'v2_tls'         => intval($request->input('v2_tls')),
+					'v2_tls'         => (int) $request->input('v2_tls'),
 					'tls_provider'   => $request->input('tls_provider')
 				];
 
@@ -300,6 +332,7 @@ class NodeController extends Controller {
 				// TODO:更新节点绑定的域名DNS(将节点IP更新到域名DNS 的A记录)
 
 				DB::commit();
+				$this->getNodeGeo($id);
 
 				return Response::json(['status' => 'success', 'message' => '编辑成功']);
 			}catch(Exception $e){
@@ -315,13 +348,13 @@ class NodeController extends Controller {
 			}
 
 			$view['node'] = $node;
-			$view['method_list'] = Helpers::methodList();
-			$view['protocol_list'] = Helpers::protocolList();
-			$view['obfs_list'] = Helpers::obfsList();
-			$view['country_list'] = Country::query()->orderBy('code')->get();
-			$view['level_list'] = Level::query()->orderBy('level')->get();
-			$view['label_list'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
-			$view['dv_list'] = NodeCertificate::query()->orderBy('id')->get();
+			$view['methodList'] = Helpers::methodList();
+			$view['protocolList'] = Helpers::protocolList();
+			$view['obfsList'] = Helpers::obfsList();
+			$view['countryList'] = Country::query()->orderBy('code')->get();
+			$view['levelList'] = Level::query()->orderBy('level')->get();
+			$view['labelList'] = Label::query()->orderByDesc('sort')->orderBy('id')->get();
+			$view['dvList'] = NodeCertificate::query()->orderBy('id')->get();
 
 			return view('admin.node.nodeInfo', $view)->with(compact('node'));
 		}
@@ -351,14 +384,20 @@ class NodeController extends Controller {
 			UserTrafficLog::query()->whereNodeId($id)->delete();
 			NodeAuth::query()->whereNodeId($id)->delete();
 			NodeRule::query()->whereNodeId($id)->delete();
-			$RuleGroupList = RuleGroup::query()->get();
-			foreach($RuleGroupList as $RuleGroup){
+			foreach(RuleGroup::all() as $RuleGroup){
 				$nodes = explode(',', $RuleGroup->nodes);
 				if(in_array($id, $nodes, true)){
 					$nodes = implode(',', array_diff($nodes, [$id]));
 					RuleGroup::query()->whereId($RuleGroup->id)->update(['nodes' => $nodes]);
 				}
 			}
+			foreach(UserGroup::all() as $UserGroup){
+				$nodes = explode(',', $UserGroup->nodes);
+				if(in_array($id, $nodes, true)){
+					$nodes = implode(',', array_diff($nodes, [$id]));
+					UserGroup::query()->whereId($UserGroup->id)->update(['nodes' => $nodes]);
+				}
+			}
 
 			DB::commit();
 

+ 1 - 2
app/Http/Controllers/User/AffiliateController.php

@@ -70,8 +70,7 @@ class AffiliateController extends Controller {
 
 		// 取出本次申请关联返利日志ID
 		$link_logs = '';
-		$referralLog = ReferralLog::uid()->whereStatus(0)->get();
-		foreach($referralLog as $log){
+		foreach(ReferralLog::uid()->whereStatus(0)->get() as $log){
 			$link_logs .= $log->id.',';
 		}
 		$link_logs = rtrim($link_logs, ',');

+ 7 - 2
app/Http/Controllers/User/SubscribeController.php

@@ -8,6 +8,7 @@ use App\Models\SsNode;
 use App\Models\User;
 use App\Models\UserSubscribe;
 use App\Models\UserSubscribeLog;
+use Arr;
 use Illuminate\Http\Request;
 use Redirect;
 use Response;
@@ -71,7 +72,11 @@ class SubscribeController extends Controller {
 		$this->subscribeLog($subscribe->id, getClientIp(), $request->headers);
 
 		// 获取这个账号可用节点
-		$query = SsNode::query()->whereStatus(1)->whereIsSubscribe(1)->where('level', '<=', $user->level);
+		$query = SsNode::query()
+		               ->whereStatus(1)
+		               ->whereIsSubscribe(1)
+		               ->groupNodePermit($user->group_id)
+		               ->where('level', '<=', $user->level);
 
 		if($this->subType === 1){
 			$query = $query->whereIn('type', [1, 4]);
@@ -86,7 +91,7 @@ class SubscribeController extends Controller {
 
 		// 打乱数组
 		if(self::$systemConfig['rand_subscribe']){
-			shuffle($nodeList);
+			$nodeList = Arr::shuffle($nodeList);
 		}
 
 		$scheme = null;

+ 16 - 10
app/Http/Controllers/UserController.php

@@ -169,6 +169,7 @@ class UserController extends Controller {
 
 	// 节点列表
 	public function nodeList(Request $request) {
+		$user = Auth::getUser();
 		if($request->isMethod('POST')){
 			$node_id = $request->input('id');
 			$infoType = $request->input('type');
@@ -180,7 +181,7 @@ class UserController extends Controller {
 			}else{
 				$proxyType = 'V2Ray';
 			}
-			$data = $this->getUserNodeInfo(Auth::id(), $node->id, $infoType !== 'text'? 0 : 1);
+			$data = $this->getUserNodeInfo($user->id, $node->id, $infoType !== 'text'? 0 : 1);
 
 			return Response::json(['status' => 'success', 'data' => $data, 'title' => $proxyType]);
 		}
@@ -188,11 +189,13 @@ class UserController extends Controller {
 		// 获取当前用户可用节点
 		$nodeList = SsNode::query()
 		                  ->whereStatus(1)
-		                  ->where('level', '<=', Auth::getUser()->level)
+		                  ->groupNodePermit($user->group_id)
+		                  ->where('level', '<=', $user->level)
 		                  ->orderByDesc('sort')
 		                  ->orderBy('id')
 		                  ->get();
 
+		$nodesGeo = $nodeList->pluck('name', 'geo')->toArray();
 		foreach($nodeList as $node){
 			$node->ct = number_format(SsNodePing::query()->whereNodeId($node->id)->where('ct', '>', '0')->avg('ct'), 1,
 				'.', '');
@@ -213,7 +216,7 @@ class UserController extends Controller {
 			$node->labels = SsNodeLabel::query()->whereNodeId($node->id)->get();
 		}
 		$view['nodeList'] = $nodeList?: [];
-
+		$view['nodesGeo'] = $nodesGeo;
 
 		return Response::view('user.nodeList', $view);
 	}
@@ -247,7 +250,7 @@ class UserController extends Controller {
 				}
 
 				// 演示环境禁止改管理员密码
-				if($user->id == 1 && env('APP_DEMO')){
+				if($user->id === 1 && config('app.demo')){
 					return Redirect::to('profile#tab_1')->withErrors('演示环境禁止修改管理员密码');
 				}
 
@@ -381,6 +384,7 @@ class UserController extends Controller {
 
 	// 添加工单
 	public function createTicket(Request $request): ?JsonResponse {
+		$user = Auth::getUser();
 		$title = $request->input('title');
 		$content = clean($request->input('content'));
 		$content = str_replace(["atob", "eval"], "", $content);
@@ -390,7 +394,7 @@ class UserController extends Controller {
 		}
 
 		$obj = new Ticket();
-		$obj->user_id = Auth::id();
+		$obj->user_id = $user->id;
 		$obj->title = $title;
 		$obj->content = $content;
 		$obj->status = 0;
@@ -398,7 +402,7 @@ class UserController extends Controller {
 
 		if($obj->id){
 			$emailTitle = "新工单提醒";
-			$content = "标题:【".$title."】<br>用户:".Auth::getUser()->email."<br>内容:".$content;
+			$content = "标题:【".$title."】<br>用户:".$user->email."<br>内容:".$content;
 
 			// 发邮件通知管理员
 			if(self::$systemConfig['webmaster_email']){
@@ -498,12 +502,13 @@ class UserController extends Controller {
 
 	// 生成邀请码
 	public function makeInvite(): JsonResponse {
-		if(Auth::getUser()->invite_num <= 0){
+		$user = Auth::getUser();
+		if($user->invite_num <= 0){
 			return Response::json(['status' => 'fail', 'message' => '生成失败:已无邀请码生成名额']);
 		}
 
 		$obj = new Invite();
-		$obj->uid = Auth::id();
+		$obj->uid = $user->id;
 		$obj->fuid = 0;
 		$obj->code = strtoupper(mb_substr(md5(microtime().makeRandStr()), 8, 12));
 		$obj->status = 0;
@@ -564,12 +569,13 @@ class UserController extends Controller {
 
 	// 购买服务
 	public function buy($goods_id) {
+		$user = Auth::getUser();
 		$goods = Goods::query()->whereId($goods_id)->whereStatus(1)->first();
 		if(empty($goods)){
 			return Redirect::to('services');
 		}
 		// 有重置日时按照重置日为标准,否者就以过期日为标准
-		$dataPlusDays = Auth::getUser()->reset_time?: Auth::getUser()->expire_time;
+		$dataPlusDays = $user->reset_time?: $user->expire_time;
 		$view['dataPlusDays'] = $dataPlusDays > date('Y-m-d')? round((strtotime($dataPlusDays) - strtotime(date('Y-m-d'))) / Day) : 0;
 		$view['activePlan'] = Order::uid()
 		                           ->with(['goods'])
@@ -588,7 +594,7 @@ class UserController extends Controller {
 	public function help(): \Illuminate\Http\Response {
 		//$view['articleList'] = Article::type(1)->orderByDesc('sort')->orderByDesc('id')->limit(10)->paginate(5);
 		$data = [];
-		if(SsNode::query()->whereIn('type',[1,4])->whereStatus(1)->exists()){
+		if(SsNode::query()->whereIn('type', [1, 4])->whereStatus(1)->exists()){
 			$data[] = 'ss';
 			//array_push
 		}

+ 2 - 2
app/Http/Middleware/isForbidden.php

@@ -29,7 +29,7 @@ class isForbidden {
 
 		// 拒绝通过订阅链接域名访问网站,防止网站被探测
 		if(true === strpos(Helpers::systemConfig()['subscribe_domain'], $request->getHost())
-		   && false === strpos(Helpers::systemConfig()['subscribe_domain'], Helpers::systemConfig()['website_url'])){
+		   && !str_contains(Helpers::systemConfig()['subscribe_domain'], Helpers::systemConfig()['website_url'])){
 			Log::info("识别到通过订阅链接访问,强制跳转至百度(".getClientIp().")");
 
 			return redirect('https://www.baidu.com');
@@ -39,7 +39,7 @@ class isForbidden {
 		if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
 			Log::info('识别到IPv6,尝试解析:'.$ip);
 			$isIPv6 = true;
-			$ipInfo = getIPv6($ip);
+			$ipInfo = getIPInfo($ip);
 		}else{
 			$isIPv6 = false;
 			$ipInfo = QQWry::ip($ip); // 通过纯真IP库解析IPv4信息

+ 1 - 1
app/Models/Article.php

@@ -10,12 +10,12 @@ use Illuminate\Database\Query\Builder;
  * 文章
  *
  * @property int                             $id
+ * @property int|null                        $type       类型:1-文章、2-站内公告、3-站外公告
  * @property string                          $title      标题
  * @property string|null                     $author     作者
  * @property string|null                     $summary    简介
  * @property string|null                     $logo       LOGO
  * @property string|null                     $content    内容
- * @property int|null                        $type       类型:1-文章、2-站内公告、3-站外公告
  * @property int                             $sort       排序
  * @property \Illuminate\Support\Carbon|null $created_at 创建时间
  * @property \Illuminate\Support\Carbon|null $updated_at 最后更新时间

+ 1 - 1
app/Models/Invite.php

@@ -21,7 +21,7 @@ use Illuminate\Database\Query\Builder;
  * @property \Illuminate\Support\Carbon|null $updated_at
  * @property \Illuminate\Support\Carbon|null $deleted_at 删除时间
  * @property-read \App\Models\User|null      $generator
- * @property-read mixed                      $status_label
+ * @property-read string                     $status_label
  * @property-read \App\Models\User|null      $user
  * @method static \Illuminate\Database\Eloquent\Builder|Invite newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder|Invite newQuery()

+ 1 - 1
app/Models/Marketing.php

@@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\Model;
  * @property int                        $status   状态:-1-失败、0-待发送、1-成功
  * @property \Illuminate\Support\Carbon $created_at
  * @property \Illuminate\Support\Carbon $updated_at
- * @property-read mixed                 $status_label
+ * @property-read string                $status_label
  * @method static Builder|Marketing newModelQuery()
  * @method static Builder|Marketing newQuery()
  * @method static Builder|Marketing query()

+ 1 - 1
app/Models/NodeCertificate.php

@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Model;
 
 /**
- * App\Models\NodeCertificate
+ * 伪装域名证书
  *
  * @property int                        $id
  * @property string                     $domain 域名

+ 2 - 2
app/Models/NotificationLog.php

@@ -11,8 +11,8 @@ use Illuminate\Database\Eloquent\Model;
  * @property int                             $id
  * @property int                             $type       类型:1-邮件、2-ServerChan、3-Bark、4-Telegram
  * @property string                          $address    收信地址
- * @property string|null                     $title      邮件标题
- * @property string|null                     $content    邮件内容
+ * @property string                          $title      标题
+ * @property string                          $content    内容
  * @property int                             $status     状态:-1发送失败、0-等待发送、1-发送成功
  * @property string|null                     $error      发送失败抛出的异常信息
  * @property \Illuminate\Support\Carbon|null $created_at 创建时间

+ 1 - 1
app/Models/Order.php

@@ -78,7 +78,7 @@ class Order extends Model {
 	}
 
 	// 订单状态
-	public function getStatusLabelAttribute() {
+	public function getStatusLabelAttribute(): string {
 		switch($this->attributes['status']){
 			case -1:
 				$status_label = trans('home.invoice_status_closed');

+ 1 - 1
app/Models/Payment.php

@@ -20,7 +20,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  * @property int                         $status   支付状态:-1-支付失败、0-等待支付、1-支付成功
  * @property \Illuminate\Support\Carbon  $created_at
  * @property \Illuminate\Support\Carbon  $updated_at
- * @property-read mixed                  $status_label
+ * @property-read string                 $status_label
  * @property-read \App\Models\Order|null $order
  * @property-read \App\Models\User       $user
  * @method static Builder|Payment newModelQuery()

+ 1 - 1
app/Models/PaymentCallback.php

@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Model;
  * @property int|null                        $status       交易状态:0-失败、1-成功
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
- * @property-read mixed                      $status_label
+ * @property-read string                     $status_label
  * @method static Builder|PaymentCallback newModelQuery()
  * @method static Builder|PaymentCallback newQuery()
  * @method static Builder|PaymentCallback query()

+ 1 - 1
app/Models/RuleGroup.php

@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Model;
  * @property string|null                     $nodes 关联的节点ID,多个用,号分隔
  * @property \Illuminate\Support\Carbon|null $created_at
  * @property \Illuminate\Support\Carbon|null $updated_at
- * @property-read mixed                      $type_label
+ * @property-read string                     $type_label
  * @method static Builder|RuleGroup newModelQuery()
  * @method static Builder|RuleGroup newQuery()
  * @method static Builder|RuleGroup query()

+ 19 - 4
app/Models/SsNode.php

@@ -11,18 +11,19 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  * SS节点信息
  *
  * @property int                                                                     $id
- * @property int                                                                     $type           服务类型:1-ShadowsocksR、2-V2ray
+ * @property int                                                                     $type           服务类型:1-Shadowsocks(R)、2-V2ray、3-Trojan、4-VNet
  * @property string                                                                  $name           名称
- * @property string|null                                                             $country_code   国家代码
+ * @property string                                                                  $country_code   国家代码
  * @property string|null                                                             $server         服务器域名地址
  * @property string|null                                                             $ip             服务器IPV4地址
  * @property string|null                                                             $ipv6           服务器IPV6地址
- * @property string|null                                                             $relay_server   中转地址
- * @property int|null                                                                $relay_port     中转端口
  * @property int                                                                     $level          等级:0-无等级,全部可见
  * @property int                                                                     $speed_limit    节点限速,为0表示不限速,单位Byte
  * @property int                                                                     $client_limit   设备数限制
+ * @property string|null                                                             $relay_server   中转地址
+ * @property int|null                                                                $relay_port     中转端口
  * @property string|null                                                             $description    节点简单描述
+ * @property string|null                                                             $geo            节点地理位置
  * @property string                                                                  $method         加密方式
  * @property string                                                                  $protocol       协议
  * @property string|null                                                             $protocol_param 协议参数
@@ -56,6 +57,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  * @property-read string                                                             $type_label
  * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\SsNodeLabel[] $label
  * @property-read int|null                                                           $label_count
+ * @method static Builder|SsNode groupNodePermit($group_id = 0)
  * @method static Builder|SsNode newModelQuery()
  * @method static Builder|SsNode newQuery()
  * @method static Builder|SsNode query()
@@ -65,6 +67,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  * @method static Builder|SsNode whereCreatedAt($value)
  * @method static Builder|SsNode whereDescription($value)
  * @method static Builder|SsNode whereDetectionType($value)
+ * @method static Builder|SsNode whereGeo($value)
  * @method static Builder|SsNode whereId($value)
  * @method static Builder|SsNode whereIp($value)
  * @method static Builder|SsNode whereIpv6($value)
@@ -118,6 +121,18 @@ class SsNode extends Model {
 		return $this->hasOne(Level::class, 'level', 'level');
 	}
 
+	// Node查询,查用户所在分组Node权限
+	public function scopeGroupNodePermit($query, $group_id = 0) {
+		$userGroup = UserGroup::find($group_id);
+		if($userGroup){
+			$nodes = explode(',', $userGroup->nodes);
+			if($nodes){
+				return $query->whereIn('id', $nodes);
+			}
+		}
+		return $query;
+	}
+
 	public function getTypeLabelAttribute(): string {
 		switch($this->attributes['type']){
 			case 1:

+ 2 - 2
app/Models/TicketReply.php

@@ -11,8 +11,8 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
  *
  * @property int                             $id
  * @property int                             $ticket_id  工单ID
- * @property int                             $user_id    回复用户ID
- * @property int|null                        $admin_id   管理员ID
+ * @property int                             $user_id    回复用户ID
+ * @property int                             $admin_id   管理员ID
  * @property string                          $content    回复内容
  * @property \Illuminate\Support\Carbon|null $created_at 创建时间
  * @property \Illuminate\Support\Carbon|null $updated_at 最后更新时间

+ 31 - 0
app/Models/User.php

@@ -38,6 +38,7 @@ use Illuminate\Notifications\Notifiable;
  * @property int                                                                                                            $ban_time        封禁到期时间
  * @property string|null                                                                                                    $remark          备注
  * @property int                                                                                                            $level           等级,默认0级
+ * @property int                                                                                                            $group_id        所属分组
  * @property int                                                                                                            $is_admin        是否管理员:0-否、1-是
  * @property string                                                                                                         $reg_ip          注册IP
  * @property int                                                                                                            $last_login      最后登录时间
@@ -48,12 +49,15 @@ use Illuminate\Notifications\Notifiable;
  * @property string|null                                                                                                    $remember_token
  * @property \Illuminate\Support\Carbon|null                                                                                $created_at
  * @property \Illuminate\Support\Carbon|null                                                                                $updated_at
+ * @property-read \App\Models\UserGroup|null                                                                                $group
  * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
  * @property-read int|null                                                                                                  $notifications_count
  * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Payment[]                                            $payment
  * @property-read int|null                                                                                                  $payment_count
  * @property-read \App\Models\User|null                                                                                     $referral
  * @property-read \App\Models\UserSubscribe|null                                                                            $subscribe
+ * @method static Builder|User activeUser()
+ * @method static Builder|User groupUserPermit($node_id = 0)
  * @method static Builder|User newModelQuery()
  * @method static Builder|User newQuery()
  * @method static Builder|User query()
@@ -66,6 +70,7 @@ use Illuminate\Notifications\Notifiable;
  * @method static Builder|User whereEnable($value)
  * @method static Builder|User whereEnableTime($value)
  * @method static Builder|User whereExpireTime($value)
+ * @method static Builder|User whereGroupId($value)
  * @method static Builder|User whereId($value)
  * @method static Builder|User whereInviteNum($value)
  * @method static Builder|User whereIp($value)
@@ -109,6 +114,14 @@ class User extends Authenticatable {
 		return $this->hasMany(Payment::class, 'user_id', 'id');
 	}
 
+	public function getLevel(): HasOne {
+		return $this->hasOne(Level::class, 'level', 'level');
+	}
+
+	public function group(): HasOne {
+		return $this->hasOne(UserGroup::class, 'id', 'group_id');
+	}
+
 	public function subscribe(): HasOne {
 		return $this->hasOne(UserSubscribe::class, 'user_id', 'id');
 	}
@@ -124,4 +137,22 @@ class User extends Authenticatable {
 	public function setCreditAttribute($value) {
 		return $this->attributes['credit'] = $value * 100;
 	}
+
+	// User查询,查那些用户有传入Node的权限
+	public function scopeGroupUserPermit($query, $node_id = 0) {
+		$groups = [0];
+		if($node_id){
+			foreach(UserGroup::all() as $userGroup){
+				$nodes = explode(',', $userGroup->nodes);
+				if(in_array($node_id, $nodes, true)){
+					$groups[] = $userGroup->id;
+				}
+			}
+		}
+		return $query->whereIn('group_id', $groups);
+	}
+
+	public function scopeActiveUser($query) {
+		return $query->where('status', '>=', 0)->whereEnable(1);
+	}
 }

+ 25 - 0
app/Models/UserGroup.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * 用户分组控制
+ *
+ * @property int         $id
+ * @property string|null $name  分组名称
+ * @property string|null $nodes 关联的节点ID,多个用,号分隔
+ * @method static Builder|UserGroup newModelQuery()
+ * @method static Builder|UserGroup newQuery()
+ * @method static Builder|UserGroup query()
+ * @method static Builder|UserGroup whereId($value)
+ * @method static Builder|UserGroup whereName($value)
+ * @method static Builder|UserGroup whereNodes($value)
+ * @mixin \Eloquent
+ */
+class UserGroup extends Model {
+	public $timestamps = false;
+	protected $table = 'user_group';
+}

+ 1 - 1
app/Models/UserTrafficLog.php

@@ -11,9 +11,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  *
  * @property int                     $id
  * @property int                     $user_id  用户ID
+ * @property int                     $node_id  节点ID
  * @property int                     $u        上传流量
  * @property int                     $d        下载流量
- * @property int                     $node_id  节点ID
  * @property float                   $rate     倍率
  * @property string                  $traffic  产生流量
  * @property int                     $log_time 记录时间

+ 3 - 4
app/helpers.php

@@ -162,10 +162,9 @@ if(!function_exists('getClientIP')){
 }
 
 // 获取IPv6信息
-if(!function_exists('getIPv6')){
-	function getIPv6($ip) {
-		$client = new Client(['timeout' => 5]);
-		$request = $client->get('https://api.ip.sb/geoip/'.$ip);
+if(!function_exists('getIPInfo')){
+	function getIPInfo($ip) {
+		$request = (new Client(['timeout' => 5]))->get('https://api.ip.sb/geoip/'.$ip);
 		$message = json_decode($request->getBody(), true);
 
 		if($request->getStatusCode() == 200){

+ 1 - 0
config/app.php

@@ -40,6 +40,7 @@ return [
     */
 
     'debug' => env('APP_DEBUG', false),
+    'demo' => env('APP_DEMO', false),
 
     /*
     |--------------------------------------------------------------------------

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/custom/maps/jquery-jvectormap-world-mill-cn.js


+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-world-mill-en.js → public/assets/custom/maps/jquery-jvectormap-world-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 42 - 37
public/assets/global/vendor/jvectormap/jquery-jvectormap.css


+ 0 - 44
public/assets/global/vendor/jvectormap/jquery-jvectormap.js

@@ -1,44 +0,0 @@
-/**
- * jVectorMap version 2.0.4
- *
- * Copyright 2011-2014, Kirill Lebedev
- *
- */
-
-(function( $ ){
-  var apiParams = {
-        set: {
-          colors: 1,
-          values: 1,
-          backgroundColor: 1,
-          scaleColors: 1,
-          normalizeFunction: 1,
-          focus: 1
-        },
-        get: {
-          selectedRegions: 1,
-          selectedMarkers: 1,
-          mapObject: 1,
-          regionName: 1
-        }
-      };
-
-  $.fn.vectorMap = function(options) {
-    var map,
-        methodName,
-        map = this.children('.jvectormap-container').data('mapObject');
-
-    if (options === 'addMap') {
-      jvm.Map.maps[arguments[1]] = arguments[2];
-    } else if ((options === 'set' || options === 'get') && apiParams[options][arguments[1]]) {
-      methodName = arguments[1].charAt(0).toUpperCase()+arguments[1].substr(1);
-      return map[options+methodName].apply(map, Array.prototype.slice.call(arguments, 2));
-    } else {
-      options = options || {};
-      options.container = this;
-      map = new jvm.Map(options);
-    }
-
-    return this;
-  };
-})( jQuery );

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/jquery-jvectormap.min.css


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/jquery-jvectormap.min.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-au-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-ca-lcc-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-de-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-europe-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-fr-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-uk_regions-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-us-merc-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-us-ny-newyork-mill-en.js


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
public/assets/global/vendor/jvectormap/maps/jquery-jvectormap-za-mill-en.js


+ 2 - 2
readme.md

@@ -7,8 +7,8 @@ Support but not limited to: Shadowsocks,ShadowsocksR,ShadowsocksRR,V2Ray,Trojan,
     - Account: test@test.com
     - Password: 123456
 - [Issues](https://github.com/ZBrettonYe/ProxyPanel/issues)
-- [WIKI](https://github.com/ZBrettonYe/ProxyPanel/wiki)
-- [Update Log](https://github.com/ZBrettonYe/ProxyPanel/wiki/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97)
+- [WIKI](https://proxypanel.gitbook.io/wiki/)
+- [UpdateLog](https://proxypanel.gitbook.io/wiki/updatelog)
 - [Upcoming](https://github.com/ZBrettonYe/ProxyPanel/projects/2)
 - [Telegram](https://t.me/joinchat/GUrO5hZsT3FOd79HAa9pcA)
 

+ 5 - 5
resources/views/admin/config/config.blade.php

@@ -44,7 +44,7 @@
 								</tr>
 								</thead>
 								<tbody>
-								@foreach($method_list as $method)
+								@foreach($methodList as $method)
 									<tr>
 										<td> {{$method->name}}</td>
 										<td>
@@ -76,7 +76,7 @@
 								</tr>
 								</thead>
 								<tbody>
-								@foreach($protocol_list as $protocol)
+								@foreach($protocolList as $protocol)
 									<tr>
 										<td> {{$protocol->name}}</td>
 										<td>
@@ -109,7 +109,7 @@
 								</tr>
 								</thead>
 								<tbody>
-								@foreach($obfs_list as $obfs)
+								@foreach($obfsList as $obfs)
 									<tr>
 										<td> {{$obfs->name}}</td>
 										<td>
@@ -140,7 +140,7 @@
 								</tr>
 								</thead>
 								<tbody>
-								@foreach($level_list as $level)
+								@foreach($levelList as $level)
 									<tr>
 										<td>
 											<input type="text" class="form-control" name="level" id="level_{{$level->id}}" value="{{$level->level}}"/>
@@ -175,7 +175,7 @@
 								</tr>
 								</thead>
 								<tbody>
-								@foreach($country_list as $country)
+								@foreach($countryList as $country)
 									<tr>
 										<td>
 											<svg class="w-40 h-40 text-center" aria-hidden="true">

+ 16 - 16
resources/views/admin/config/system.blade.php

@@ -414,6 +414,21 @@
 											<span class="text-help offset-md-3"> 用户自行生成邀请的有效期 </span>
 										</div>
 									</div>
+									<div class="form-group col-lg-6">
+										<div class="row">
+											<label class="col-md-3 col-form-label" for="admin_invite_days">管理员-邀请码有效期</label>
+											<div class="col-md-7">
+												<div class="input-group">
+													<input type="number" class="form-control" id="admin_invite_days" value="{{$admin_invite_days}}"/>
+													<div class="input-group-append">
+														<span class="input-group-text">天</span>
+													</div>
+													<button class="btn btn-primary" type="button" onclick="updateFromInput('admin_invite_days','1',false)">修改</button>
+												</div>
+											</div>
+											<span class="text-help offset-md-3"> 管理员生成邀请码的有效期 </span>
+										</div>
+									</div>
 								</div>
 							</form>
 						</div>
@@ -551,21 +566,6 @@
 											</span>
 										</div>
 									</div>
-									<div class="form-group col-lg-6">
-										<div class="row">
-											<label class="col-md-3 col-form-label" for="admin_invite_days">管理员-邀请码有效期</label>
-											<div class="col-md-7">
-												<div class="input-group">
-													<input type="number" class="form-control" id="admin_invite_days" value="{{$admin_invite_days}}"/>
-													<div class="input-group-append">
-														<span class="input-group-text">天</span>
-													</div>
-													<button class="btn btn-primary" type="button" onchange="updateFromInput('admin_invite_days','1',false)">修改</button>
-												</div>
-											</div>
-											<span class="text-help offset-md-3"> 管理员生成邀请码的有效期 </span>
-										</div>
-									</div>
 									<div class="form-group col-lg-6">
 										<div class="row">
 											<label class="col-md-3 col-form-label" for="is_captcha">验证码</label>
@@ -760,7 +760,7 @@
 													<input type="number" class="form-control" id="referral_percent" value="{{$referral_percent * 100}}"/>
 													<div class="input-group-append">
 														<span class="input-group-text">%</span>
-														<button class="btn btn-primary" type="button" onchange="updateFromInput('referral_percent','0','100')">修改</button>
+														<button class="btn btn-primary" type="button" onclick="updateFromInput('referral_percent','0','100')">修改</button>
 													</div>
 												</div>
 											</div>

+ 124 - 0
resources/views/admin/group/groupInfo.blade.php

@@ -0,0 +1,124 @@
+@extends('admin.layouts')
+@section('css')
+	<link href="/assets/global/vendor/multi-select/multi-select.min.css" type="text/css" rel="stylesheet">
+@endsection
+@section('content')
+	<div class="page-content container">
+		<div class="panel">
+			<div class="panel-heading">
+				<h2 class="panel-title">@isset($userGroup)编辑@else添加@endisset用戶分组</h2>
+				<div class="panel-actions">
+					<a href="/group" class="btn btn-danger">返 回</a>
+				</div>
+			</div>
+			@if (Session::has('successMsg'))
+				<div class="alert alert-success alert-dismissible">
+					<button type="button" class="close" data-dismiss="alert" aria-label="Close">
+						<span aria-hidden="true">×</span></button>
+					{{Session::get('successMsg')}}
+				</div>
+			@endif
+			@if($errors->any())
+				<div class="alert alert-danger alert-dismissible">
+					<button type="button" class="close" data-dismiss="alert" aria-label="Close">
+						<span aria-hidden="true">×</span></button>
+					<ul>
+						@foreach ($errors->all() as $error)
+							<li>{{ $error }}</li>
+						@endforeach
+					</ul>
+				</div>
+			@endif
+			<div class="panel-body">
+				<form action=@isset($userGroup){{url('/group/edit')}}@else{{url('/group/add')}}@endisset method="POST" enctype="multipart/form-data" class="form-horizontal">
+					@isset($userGroup)<input name="id" value="{{$userGroup->id}}" hidden/>@endisset
+					@csrf
+					<div class="form-group row">
+						<label class="col-md-2 col-sm-3 col-form-label" for="name">分组名称</label>
+						<div class="col-md-9 col-sm-9">
+							<input type="text" class="form-control" name="name" id="name"/>
+						</div>
+					</div>
+					<div class="form-group row">
+						<label class="col-md-2 col-sm-3 col-form-label" for="nodes">选择节点</label>
+						<div class="col-md-9 col-sm-9">
+							<div class="btn-group mb-20">
+								<button type="button" class="btn btn-primary" id="select-all">全 选</button>
+								<button type="button" class="btn btn-danger" id="deselect-all">清 空</button>
+							</div>
+							<select class="form-control" name="nodes[]" id="nodes" data-plugin="multiSelect" multiple>
+								@foreach($nodeList as $node)
+									<option value="{{$node->id}}">{{$node->id . ' - ' . $node->name}}</option>
+								@endforeach
+							</select>
+						</div>
+					</div>
+					<div class="form-actions text-right">
+						<button type="submit" class="btn btn-success">提 交</button>
+					</div>
+				</form>
+			</div>
+		</div>
+	</div>
+@endsection
+@section('script')
+	<script src="/assets/global/vendor/multi-select/jquery.multi-select.js" type="text/javascript"></script>
+	<script src="/assets/global/js/Plugin/multi-select.js"></script>
+	<script src="/assets/global/js/jquery.quicksearch.js" type="text/javascript"></script>
+	<script type="text/javascript">
+		@isset($userGroup)
+		$(document).ready(function () {
+			$('#name').val('{{$userGroup->name}}');
+			$('#nodes').multiSelect('select',{!! json_encode(explode(',', $userGroup->nodes)) !!});
+		})
+		@endisset
+		// 权限列表
+		$('#nodes').multiSelect({
+			selectableHeader: "<input type='text' class='search-input form-control' autocomplete='off' placeholder='待分配规则,此处可搜索'>",
+			selectionHeader: "<input type='text' class='search-input form-control' autocomplete='off' placeholder='已分配规则,此处可搜索'>",
+			afterInit: function () {
+				const that = this,
+					$selectableSearch = that.$selectableUl.prev(),
+					$selectionSearch = that.$selectionUl.prev(),
+					selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
+					selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';
+
+				that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
+					.on('keydown', function (e) {
+						if (e.which === 40) {
+							that.$selectableUl.focus();
+							return false;
+						}
+					});
+
+				that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
+					.on('keydown', function (e) {
+						if (e.which === 40) {
+							that.$selectionUl.focus();
+							return false;
+						}
+					});
+			},
+			afterSelect: function () {
+				this.qs1.cache();
+				this.qs2.cache();
+			},
+			afterDeselect: function () {
+				this.qs1.cache();
+				this.qs2.cache();
+			}
+		});
+
+		// 全选
+		$('#select-all').click(function () {
+			$('#node').multiSelect('select_all');
+			return false;
+		});
+
+		// 反选
+		$('#deselect-all').click(function () {
+			$('#node').multiSelect('deselect_all');
+			return false;
+		});
+	</script>
+@endsection

+ 94 - 0
resources/views/admin/group/groupList.blade.php

@@ -0,0 +1,94 @@
+@extends('admin.layouts')
+@section('css')
+	<link href="/assets/global/vendor/bootstrap-table/bootstrap-table.min.css" type="text/css" rel="stylesheet">
+@endsection
+@section('content')
+	<div class="page-content container-fluid">
+		<div class="panel">
+			<div class="panel-heading">
+				<h2 class="panel-title">用户分组控制<small>(同一节点可分配至多个分组,一个用户只能属于一个分组;对于用户可见/可用节点:先按分组后按等级)</small></h2>
+				<div class="panel-actions">
+					<a class="btn btn-primary" href="/group/add">
+						<i class="icon wb-plus" aria-hidden="true"></i>添加分组
+					</a>
+				</div>
+			</div>
+			<div class="panel-body">
+				<table class="text-md-center" data-toggle="table" data-mobile-responsive="true">
+					<thead class="thead-default">
+					<tr>
+						<th> #</th>
+						<th> 分组名称</th>
+						<th> 操作</th>
+					</tr>
+					</thead>
+					<tbody>
+					@foreach ($list as $vo)
+						<tr>
+							<td> {{$vo->id}} </td>
+							<td> {{$vo->name}} </td>
+							<td>
+								<div class="btn-group">
+									<a href="/group/edit?id={{$vo->id}}" class="btn btn-primary">
+										<i class="icon wb-edit" aria-hidden="true"></i>
+									</a>
+									<button onclick="deleteUserGroup('{{$vo->id}}')" class="btn btn-danger">
+										<i class="icon wb-trash" aria-hidden="true"></i>
+									</button>
+								</div>
+							</td>
+						</tr>
+					@endforeach
+					</tbody>
+				</table>
+			</div>
+			<div class="panel-footer">
+				<div class="row">
+					<div class="col-sm-4">
+						共 <code>{{$list->total()}}</code> 个分组
+					</div>
+					<div class="col-sm-8">
+						<nav class="Page navigation float-right">
+							{{$list->links()}}
+						</nav>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+@endsection
+@section('script')
+	<script src="/assets/global/vendor/bootstrap-table/bootstrap-table.min.js" type="text/javascript"></script>
+	<script src="/assets/global/vendor/bootstrap-table/extensions/mobile/bootstrap-table-mobile.min.js" type="text/javascript"></script>
+
+	<script type="text/javascript">
+		// 删除用户分组
+		function deleteUserGroup(id) {
+			swal.fire({
+				title: '提示',
+				text: '确定删除该分组吗?',
+				type: 'info',
+				showCancelButton: true,
+				cancelButtonText: '{{trans('home.ticket_close')}}',
+				confirmButtonText: '{{trans('home.ticket_confirm')}}',
+			}).then((result) => {
+				if (result.value) {
+					$.ajax({
+						type: 'DELETE',
+						url: '/group/delete',
+						data: {_token: '{{csrf_token()}}', id: id},
+						dataType: 'json',
+						success: function (ret) {
+							if (ret.status === 'success') {
+								swal.fire({title: ret.message, type: 'success', timer: 1000, showConfirmButton: false})
+									.then(() => window.location.reload())
+							} else{
+								swal.fire({title: ret.message, type: "error"}).then(() => window.location.reload())
+							}
+						}
+					});
+				}
+			});
+		}
+	</script>
+@endsection

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

@@ -113,7 +113,7 @@
 					<span class="site-menu-title">管理中心</span>
 				</a>
 			</li>
-			<li class="site-menu-item has-sub {{in_array(Request::path(), ['admin/userList', 'admin/addUser', 'admin/editUser', 'admin/export', 'admin/userMonitor', 'admin/userCreditLogList', 'subscribe']) ? 'active open' : ''}}">
+			<li class="site-menu-item has-sub {{in_array(Request::path(), ['admin/userList', 'admin/addUser', 'admin/editUser', 'admin/export', 'admin/userMonitor', 'group', 'group/add', 'group/edit', 'admin/userCreditLogList', 'subscribe']) ? 'active open' : ''}}">
 				<a href="javascript:void(0)">
 					<i class="site-menu-icon wb-user" aria-hidden="true"></i>
 					<span class="site-menu-title">用户系统</span>
@@ -124,6 +124,11 @@
 							<span class="site-menu-title">用户管理</span>
 						</a>
 					</li>
+					<li class="site-menu-item {{in_array(Request::path(), ['group', 'group/add', 'group/edit']) ? 'active open' : ''}}">
+						<a href="/group">
+							<span class="site-menu-title">用戶分组</span>
+						</a>
+					</li>
 					<li class="site-menu-item {{in_array(Request::path(), ['admin/userCreditLogList']) ? 'active open' : ''}}">
 						<a href="/admin/userCreditLogList">
 							<span class="site-menu-title">余额变动</span>

+ 9 - 9
resources/views/admin/node/nodeInfo.blade.php

@@ -70,7 +70,7 @@
 									<div class="form-group row">
 										<label for="level" class="col-md-3 col-form-label">等级</label>
 										<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="level" name="level">
-											@foreach($level_list as $level)
+											@foreach($levelList as $level)
 												<option value="{{$level->level}}">{{$level->name}}</option>
 											@endforeach
 										</select>
@@ -90,7 +90,7 @@
 									<div class="form-group row">
 										<label for="labels" class="col-md-3 col-form-label">标签</label>
 										<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control show-tick" id="labels" name="labels" multiple>
-											@foreach($label_list as $label)
+											@foreach($labelList as $label)
 												<option value="{{$label->id}}">{{$label->name}}</option>
 											@endforeach
 										</select>
@@ -100,7 +100,7 @@
 										<select data-plugin="selectpicker" data-style="btn-outline btn-primary"
 												class="col-md-5 form-control" name="country_code" id="country_code">
 											<option value="un" selected hidden>请选择</option>
-											@foreach($country_list as $country)
+											@foreach($countryList as $country)
 												<option value="{{$country->code}}">{{$country->code}}- {{$country->name}}</option>
 											@endforeach
 										</select>
@@ -168,7 +168,7 @@
 										<div class="form-group row">
 											<label for="method" class="col-md-3 col-form-label">加密方式</label>
 											<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control" name="method" id="method">
-												@foreach ($method_list as $method)
+												@foreach ($methodList as $method)
 													<option value="{{$method->name}}" @if(!isset($node) && $method->is_default) selected @endif>{{$method->name}}</option>
 												@endforeach
 											</select>
@@ -176,7 +176,7 @@
 										<div class="form-group row">
 											<label for="protocol" class="col-md-3 col-form-label">协议</label>
 											<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control" name="protocol" id="protocol">
-												@foreach ($protocol_list as $protocol)
+												@foreach ($protocolList as $protocol)
 													<option value="{{$protocol->name}}" @if(!isset($node) && $protocol->is_default) selected @endif>{{$protocol->name}}</option>
 												@endforeach
 											</select>
@@ -188,7 +188,7 @@
 										<div class="form-group row">
 											<label for="obfs" class="col-md-3 col-form-label">混淆</label>
 											<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="col-md-5 form-control" name="obfs" id="obfs">
-												@foreach ($obfs_list as $obfs)
+												@foreach ($obfsList as $obfs)
 													<option value="{{$obfs->name}}" @if(!isset($node) && $obfs->is_default) selected @endif>{{$obfs->name}}</option>
 												@endforeach
 											</select>
@@ -287,7 +287,7 @@
 												<div name="v2_ws">
 													<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="form-control" id="v2_ws">
 														<option value="" hidden></option>
-														@foreach($dv_list as $dv)
+														@foreach($dvList as $dv)
 															<option value="{{$dv->domain}}" @if(isset($node) && $node->v2_net == "ws" && $node->v2_host == $dv->domain) selected @endif>{{$dv->domain}}</option>
 														@endforeach
 													</select>
@@ -309,8 +309,8 @@
 											<label for="tls_provider" class="col-md-3 col-form-label">TLS配置</label>
 											<input type="text" class="form-control col-md-9" name="tls_provider" id="tls_provider"/>
 											<div class="text-help offset-md-3"> 不同后端配置不同:
-												<a href="https://github.com/Scyllaly/docs/wiki/tls" target="_blank">VNET-V2Ray</a> 、
-												<a href="https://github.com/ColetteContreras/v2ray-poseidon/wiki/020-%E5%AF%B9%E6%8E%A5-VNetPanel-%E6%95%99%E7%A8%8B#%E9%85%8D%E7%BD%AE-tls-%E8%AF%81%E4%B9%A6" target="_blank">V2Ray-Poseidon</a>
+												<a href="https://proxypanel.gitbook.io/wiki/webapi/webapi-basic-setting#vnet-v2-ray-hou-duan" target="_blank">VNET-V2Ray</a> 、
+												<a href="https://proxypanel.gitbook.io/wiki/webapi/webapi-basic-setting#v-2-ray-poseidon-hou-duan" target="_blank">V2Ray-Poseidon</a>
 											</div>
 										</div>
 									</div>

+ 34 - 7
resources/views/admin/node/nodeList.blade.php

@@ -12,7 +12,10 @@
 		<div class="panel">
 			<div class="panel-heading">
 				<h3 class="panel-title">节点列表</h3>
-				<div class="panel-actions">
+				<div class="panel-actions btn-group">
+					<button type="button" onclick="refreshGeo()" class="btn btn-info">
+						<i class="icon wb-map"></i> 刷新节点地理信息
+					</button>
 					<a href="/node/add" class="btn btn-primary"><i class="icon wb-plus"></i> 添加节点</a>
 				</div>
 			</div>
@@ -63,6 +66,9 @@
 							</td>
 							<td>
 								<div class="btn-group">
+									<button type="button" onclick="refreshGeo('{{$node->id}}')" class="btn btn-primary">
+										<i id="geo{{$node->id}}" class="icon wb-map"></i>
+									</button>
 									<a href="javascript:pingNode('{{$node->id}}')" class="btn btn-primary">
 										<i id="ping{{$node->id}}" class="icon wb-order"></i>
 									</a>
@@ -102,10 +108,9 @@
 @endsection
 @section('script')
 	<script src="/assets/global/vendor/bootstrap-table/bootstrap-table.min.js" type="text/javascript"></script>
-	<script src="/assets/global/vendor/bootstrap-table/extensions/mobile/bootstrap-table-mobile.min.js"
-			type="text/javascript"></script>
+	<script src="/assets/global/vendor/bootstrap-table/extensions/mobile/bootstrap-table-mobile.min.js" type="text/javascript"></script>
 	<script type="text/javascript">
-		//节点连通性测试
+		// 节点连通性测试
 		function checkNode(id) {
 			$.ajax({
 				type: "POST",
@@ -123,7 +128,7 @@
 							showConfirmButton: false
 						})
 					} else {
-						swal.fire({title: ret.title, type: "error"})
+						swal.fire({title: ret.title, text: ret.message, type: "error"})
 					}
 				},
 				complete: function () {
@@ -132,7 +137,7 @@
 			});
 		}
 
-		//Ping 节点获取延迟
+		// Ping节点获取延迟
 		function pingNode(id) {
 			$.ajax({
 				type: "POST",
@@ -149,7 +154,7 @@
 							showConfirmButton: false
 						})
 					} else {
-						swal.fire({title: ret.title, type: "error"})
+						swal.fire({title: ret.message, type: "error"})
 					}
 				},
 				complete: function () {
@@ -158,6 +163,28 @@
 			});
 		}
 
+		// 刷新节点地理信息
+		function refreshGeo(id) {
+			$.ajax({
+				type: "GET",
+				url: '/node/refreshGeo',
+				data: {_token: '{{csrf_token()}}', id: id},
+				beforeSend: function () {
+					$("#geo" + id).removeClass("wb-map").addClass("wb-loop icon-spin");
+				},
+				success: function (ret) {
+					if (ret.status === 'success') {
+						swal.fire({type: 'info', title: ret.message, showConfirmButton: false})
+					} else {
+						swal.fire({title: ret.message, type: "error"})
+					}
+				},
+				complete: function () {
+					$("#geo" + id).removeClass("wb-loop icon-spin").addClass("wb-map");
+				}
+			});
+		}
+
 		// 删除节点
 		function delNode(id, name) {
 			swal.fire({

+ 11 - 9
resources/views/admin/shop/goodsInfo.blade.php

@@ -59,7 +59,7 @@
 										<label for="data_package">流量包</label>
 									</div>
 									<div class="radio-custom radio-primary radio-inline">
-										<input type="radio" name="type" id="data_plan" value="2"/>
+										<input type="radio" name="type" id="data_plan" value="2" checked/>
 										<label for="data_plan">套餐</label>
 									</div>
 								</div>
@@ -82,7 +82,7 @@
 								<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="level" id="level">
-										@foreach ($level_list as $level)
+										@foreach ($levelList as $level)
 											<option value="{{$level->level}}">{{$level->name}}</option>
 										@endforeach
 									</select>
@@ -98,7 +98,7 @@
 							<div class="form-group row package-renew">
 								<label class="col-md-2 col-form-label" for="period">重置周期</label>
 								<div class="col-md-4 input-group">
-									<input type="number" class="form-control" name="period" id="period" value="0"/>
+									<input type="number" class="form-control" name="period" id="period" value="30"/>
 									<span class="input-group-text">天</span>
 								</div>
 								<span class="text-help"> 套餐流量会每N天重置 </span>
@@ -106,21 +106,21 @@
 							<div class="form-group row">
 								<label class="col-md-2 col-form-label" for="traffic">流量额度</label>
 								<div class="col-md-4 input-group">
-									<input type="number" class="form-control" name="traffic" id="traffic"/>
+									<input type="number" class="form-control" name="traffic" id="traffic" value="100"/>
 									<span class="input-group-text">MB</span>
 								</div>
 							</div>
 							<div class="form-group row">
 								<label class="col-md-2 col-form-label" for="invite_num">赠送邀请码数量</label>
 								<div class="col-md-4 input-group">
-									<input type="number" class="form-control" name="invite_num" id="invite_num" required/>
+									<input type="number" class="form-control" name="invite_num" id="invite_num" value="0" required/>
 									<span class="input-group-text">枚</span>
 								</div>
 							</div>
 							<div class="form-group row">
 								<label class="col-md-2 col-form-label" for="limit_num">限购数量</label>
 								<div class="col-md-4 input-group">
-									<input type="number" class="form-control" name="limit_num" id="limit_num" required/>
+									<input type="number" class="form-control" name="limit_num" id="limit_num" value="0" required/>
 									<span class="input-group-text">次</span>
 								</div>
 								<span class="text-help"> 每个用户可以购买该商品次数,为 0 时代表不限购 </span>
@@ -128,7 +128,7 @@
 							<div class="form-group row package-renew">
 								<label class="col-md-2 col-form-label" for="days">有效期</label>
 								<div class="col-md-4 input-group">
-									<input type="number" class="form-control" name="days" id="days" value="0"/>
+									<input type="number" class="form-control" name="days" id="days" value="30"/>
 									<span class="input-group-text">天</span>
 								</div>
 								<span class="text-help"> 到期后会自动从总流量扣减对应的流量 </span>
@@ -150,14 +150,14 @@
 							<div class="form-group row">
 								<label class="col-md-2 col-form-label" for="sort">排序</label>
 								<div class="col-md-4">
-									<input type="number" class="form-control" name="sort" id="sort"/>
+									<input type="number" class="form-control" name="sort" id="sort" value="0"/>
 								</div>
 								<span class="text-help"> 排序值越大排越前 </span>
 							</div>
 							<div class="form-group row">
 								<label class="col-md-2 col-form-label" for="color">颜色</label>
 								<div class="col-md-4">
-									<input type="text" class="form-control" name="color" id="color" data-plugin="asColorPicker" data-mode="simple"/>
+									<input type="text" class="form-control" name="color" id="color" data-plugin="asColorPicker" data-mode="simple" value="#A57AFA"/>
 								</div>
 							</div>
 							<div class="form-group row">
@@ -257,6 +257,8 @@
 			$("#description").val('{{old('description')}}')
 			$("#info").val('{{old('info')}}')
 		})
+		@else
+		$("#status").click()
 
 		@endisset
 

+ 3 - 3
resources/views/admin/tools/convert.blade.php

@@ -12,7 +12,7 @@
 					<div class="col-md-4 form-group">
 						<label for="method">加密方式</label>
 						<select class="form-control" name="method" id="method">
-							@foreach ($method_list as $method)
+							@foreach ($methodList as $method)
 								<option value="{{$method->name}}"
 										@if($method->is_default) selected @endif>{{$method->name}}</option>
 							@endforeach
@@ -29,7 +29,7 @@
 					<div class="col-md-4 form-group">
 						<label for="protocol">协议</label>
 						<select class="form-control" name="protocol" id="protocol">
-							@foreach ($protocol_list as $protocol)
+							@foreach ($protocolList as $protocol)
 								<option value="{{$protocol->name}}"
 										@if($protocol->is_default) selected @endif>{{$protocol->name}}</option>
 							@endforeach
@@ -44,7 +44,7 @@
 					<div class="col-md-4 form-group">
 						<label for="obfs">混淆</label>
 						<select class="form-control" name="obfs" id="obfs">
-							@foreach ($obfs_list as $obfs)
+							@foreach ($obfsList as $obfs)
 								<option value="{{$obfs->name}}"
 										@if($obfs->is_default) selected @endif>{{$obfs->name}}</option>
 							@endforeach

+ 18 - 5
resources/views/admin/user/userInfo.blade.php

@@ -41,12 +41,23 @@
 								<label class="col-md-2 col-sm-3 col-form-label" for="level">级别</label>
 								<div class="col-xl-4 col-sm-8">
 									<select class="form-control" name="level" id="level" data-plugin="selectpicker" data-style="btn-outline btn-primary">
-										@foreach($level_list as $level)
+										@foreach($levelList as $level)
 											<option value="{{$level->level}}">{{$level->name}}</option>
 										@endforeach
 									</select>
 								</div>
 							</div>
+							<div class="form-group row">
+								<label class="col-md-2 col-sm-3 col-form-label" for="group">分组</label>
+								<div class="col-xl-4 col-sm-8">
+									<select class="form-control" name="group" id="group" data-plugin="selectpicker" data-style="btn-outline btn-primary">
+										<option value="0">无分组</option>
+										@foreach($groupList as $group)
+											<option value="{{$group->id}}">{{$group->name}}</option>
+										@endforeach
+									</select>
+								</div>
+							</div>
 							@isset($user)
 								<div class="form-group row">
 									<label class="col-md-2 col-sm-3 col-form-label" for="credit">余额</label>
@@ -65,7 +76,7 @@
 							<div class="form-group row">
 								<label class="col-md-2 col-sm-3 col-form-label" for="invite_num">可用邀请码</label>
 								<div class="col-xl-6 col-sm-8">
-									<input type="number" class="form-control" name="invite_num" id="invite_num" required/>
+									<input type="number" class="form-control" name="invite_num" id="invite_num" value="0" required/>
 								</div>
 							</div>
 							<div class="form-group row">
@@ -219,7 +230,7 @@
 								<label class="col-md-2 col-sm-3 col-form-label" for="method">加密方式</label>
 								<div class="col-xl-5 col-sm-8">
 									<select class="form-control" name="method" id="method" data-plugin="selectpicker" data-style="btn-outline btn-primary">
-										@foreach ($method_list as $method)
+										@foreach ($methodList as $method)
 											<option value="{{$method->name}}">{{$method->name}}</option>
 										@endforeach
 									</select>
@@ -259,7 +270,7 @@
 								<div class="col-xl-5 col-sm-8">
 									<select class="form-control" name="protocol" id="protocol"
 											data-plugin="selectpicker" data-style="btn-outline btn-primary">
-										@foreach ($protocol_list as $protocol)
+										@foreach ($protocolList as $protocol)
 											<option value="{{$protocol->name}}"
 													@if($protocol->is_default) selected @endif>{{$protocol->name}}</option>
 										@endforeach
@@ -270,7 +281,7 @@
 								<label class="col-md-2 col-sm-3 col-form-label" for="obfs">混淆</label>
 								<div class="col-xl-5 col-sm-8">
 									<select data-plugin="selectpicker" data-style="btn-outline btn-primary" class="form-control" name="obfs" id="obfs">
-										@foreach ($obfs_list as $obfs)
+										@foreach ($obfsList as $obfs)
 											<option value="{{$obfs->name}}"
 													@if($obfs->is_default) selected @endif>{{$obfs->name}}</option>
 										@endforeach
@@ -336,6 +347,7 @@
 			$('#username').val('{{$user->username}}')
 			$('#email').val('{{$user->email}}')
 			$('#level').selectpicker('val', '{{$user->level}}')
+			$('#group').selectpicker('val', '{{$user->group_id}}')
 			$('#invite_num').val('{{$user->invite_num}}')
 			$('#reset_time').val('{{$user->reset_time}}')
 			$('#enable_time').val('{{$user->enable_time}}')
@@ -462,6 +474,7 @@
 					expire_time: $('#expire_time').val(),
 					remark: $('#remark').val(),
 					level: $("#level").val(),
+					group_id: $("#group").val(),
 					is_admin: $("input:radio[name='is_admin']:checked").val(),
 					reset_time: $('#reset_time').val(),
 					invite_num: $('#invite_num').val(),

+ 2 - 2
resources/views/components/avatar.blade.php

@@ -1,7 +1,7 @@
 @if($user->qq)
-	<img src="http://q1.qlogo.cn/g?b=qq&nk={{$user->qq}}&s=640" alt="头像">
+	<img src="https://q1.qlogo.cn/g?b=qq&nk={{$user->qq}}&s=640" alt="头像">
 @elseif(strpos(strtolower($user->email),"@qq.com") !== false)
-	<img src="http://q1.qlogo.cn/g?b=qq&nk={{$user->email}}&s=640" alt="头像">
+	<img src="https://q1.qlogo.cn/g?b=qq&nk={{$user->email}}&s=640" alt="头像">
 @else
 	<img src="/assets/images/avatar.svg" alt="头像">
 @endif

+ 2 - 2
resources/views/user/index.blade.php

@@ -22,8 +22,8 @@
 						</button>
 						<span class="font-weight-400">{{trans('home.account_status')}}</span>
 						@if(\App\Components\Helpers::systemConfig()['is_checkin'])
-							<a class="btn btn-md btn-round btn-info float-right" href="javascript:checkIn();"><i
-										class="wb-star yellow-400 mr-5"></i>
+							<a class="btn btn-md btn-round btn-info float-right" href="javascript:checkIn();">
+								<i class="wb-star yellow-400 mr-5"></i>
 								{{trans('home.sign_in')}}
 							</a>
 						@endif

+ 100 - 4
resources/views/user/nodeList.blade.php

@@ -3,14 +3,63 @@
 	<script src="//at.alicdn.com/t/font_682457_e6aq10jsbq0yhkt9.js" type="text/javascript"></script>
 	<link href="/assets/global/fonts/font-awesome/font-awesome.min.css" type="text/css" rel="stylesheet">
 	<link href="/assets/global/vendor/webui-popover/webui-popover.min.css" type="text/css" rel="stylesheet">
+	<link href="/assets/global/vendor/jvectormap/jquery-jvectormap.css" type="text/css" rel="stylesheet">
 @endsection
 @section('content')
 	<!-- BEGIN CONTENT BODY -->
 	<div class="page-content container-fluid">
 		<div class="row">
+			<div class="col-md-9">
+				<div class="card card-inverse card-shadow bg-white map">
+					<div class="card-block h-450">
+						<div class="h-p100" id="world-map"></div>
+					</div>
+				</div>
+			</div>
+			<div class="col-md-3">
+				<div class="row map">
+					<div class="col-md-12">
+						<div class="card card-block p-20  bg-indigo-500">
+							<div class="counter counter-lg counter-inverse">
+								<div class="counter-label text-uppercase font-size-16">账号等级</div>
+								<div class="counter-number-group">
+									<span class="counter-icon"><i class="icon wb-user-circle" aria-hidden="true"></i></span>
+									<span class="counter-number ml-10">{{Auth::getUser()->level}}</span>
+								</div>
+								<div class="counter-label text-uppercase font-size-16">{{Auth::getUser()->getLevel->name}}</div>
+							</div>
+						</div>
+					</div>
+					@if(Auth::getUser()->group_id)
+						<div class="col-md-12">
+							<div class="card card-block p-20 bg-indigo-500">
+								<div class="counter counter-lg counter-inverse">
+									<div class="counter-label text-uppercase font-size-16">所属分组</div>
+									<div class="counter-number-group">
+										<span class="counter-icon"><i class="icon wb-globe" aria-hidden="true"></i></span>
+										<span class="counter-number ml-10">{{Auth::getUser()->group->name}}</span>
+									</div>
+								</div>
+							</div>
+						</div>
+					@endif
+					<div class="col-md-12">
+						<div class="card card-block p-20 bg-indigo-500">
+							<div class="counter counter-lg counter-inverse">
+								<div class="counter-label text-uppercase font-size-16">限速</div>
+								<div class="counter-number-group">
+									<span class="counter-icon"><i class="icon wb-signal" aria-hidden="true"></i></span>
+									<span class="counter-number ml-10">{{Auth::getUser()->speed_limit?:'无限制'}}</span>
+								</div>
+								<div class="counter-label font-size-16">Mbps</div>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
 			@foreach($nodeList as $node)
 				<div class="col-xxl-3 col-xl-4 col-sm-6">
-					<div class="card card-inverse card-shadow bg-white">
+					<div class="card card-inverse card-shadow bg-white node">
 						<div class="card-block p-30 row">
 							<div class="col-4">
 								<svg class="w-p100 text-center" aria-hidden="true">
@@ -21,7 +70,7 @@
 								<p class="font-size-20 blue-600">
 									<span class="badge badge-pill up m-0 badge-default">{{$node->getLevel->name}}</span>
 									@if($node->offline)
-										<i class="red-600 icon wb-warning" data-content="线路不稳定/维护中" data-trigger="hover" data-toggle="popover" data-placement="top"></i>
+										<i class="red-600 icon wb-warning" data-content="线路波动/维护中" data-trigger="hover" data-toggle="popover" data-placement="top"></i>
 									@endif
 									@if($node->traffic_rate != 1)
 										<i class="green-600 icon wb-info-circle" data-content="{{$node->traffic_rate}} 倍流量消耗" data-trigger="hover" data-toggle="popover" data-placement="top"></i>
@@ -60,15 +109,62 @@
 			@endforeach
 		</div>
 	</div>
-@endsection @section('script')
+@endsection
+@section('script')
 	<script src="/assets/global/vendor/matchheight/jquery.matchHeight-min.js" type="text/javascript"></script>
 	<script src="/assets/global/js/Plugin/matchheight.js" type="text/javascript"></script>
 	<script src="/assets/custom/Plugin/jquery-qrcode/jquery.qrcode.min.js" type="text/javascript"></script>
 	<script src="/assets/global/js/Plugin/webui-popover.js" type="text/javascript"></script>
+	<script src="/assets/global/vendor/jvectormap/jquery-jvectormap.min.js"></script>
+	<script src="/assets/custom/maps/jquery-jvectormap-world-mill-cn.js"></script>
+
 
 	<script type="text/javascript">
 		$(function () {
-			$('.card').matchHeight();
+			$('#world-map').vectorMap({
+				map: 'world_mill',
+				scaleColors: ['#C8EEFF', '#0071A4'],
+				normalizeFunction: 'polynomial',
+				zoomAnimate: true,
+				hoverOpacity: 0.7,
+				hoverColor: false,
+				regionStyle: {
+					initial: {
+						fill: '#3E8EF7'
+					},
+					hover: {
+						fill: '#589FFC'
+					},
+					selected: {
+						fill: '#0B69E3'
+					},
+					selectedHover: {
+						fill: '#589FFC'
+					}
+				},
+				markerStyle: {
+					initial: {
+						r: 3,
+						fill: '#FF4C52',
+						'stroke-width': 0
+					},
+					hover: {
+						r: 6,
+						stroke: '#FF4C52',
+						'stroke-width': 0
+					}
+				},
+				backgroundColor: '#fff',
+				markers: [
+						@foreach($nodesGeo as $key => $geo)
+					{
+						latLng: [{{$key}}], name: '{{$geo}}'
+					},
+					@endforeach
+				]
+			});
+			$('.node').matchHeight();
+			$('.map').matchHeight();
 		});
 
 		function getInfo(id, type) {

+ 8 - 0
routes/web.php

@@ -87,6 +87,7 @@ Route::group(['middleware' => ['isForbidden', 'isAdminLogin', 'isAdmin']], funct
 		Route::post('check', 'NodeController@checkNode'); // 节点阻断检测
 		Route::post('ping', 'NodeController@pingNode'); // 节点ping测速
 		Route::get('pingLog', 'NodeController@pingLog'); //节点Ping测速日志
+		Route::get('refreshGeo', 'NodeController@refreshGeo'); //更新节点
 		// 节点Api授权相关
 		Route::group(['prefix' => 'auth'], function() {
 			Route::get('/', 'NodeController@authList'); // 节点授权列表
@@ -168,6 +169,13 @@ Route::group(['middleware' => ['isForbidden', 'isAdminLogin', 'isAdmin']], funct
 			Route::get('log', 'RuleController@ruleLogList'); // 用户触发审计规则日志
 			Route::post('clear', 'RuleController@clearLog'); // 清除所有审计触发日志
 		});
+
+		Route::group(['prefix' => 'group'], function() {
+			Route::get('/', 'GroupController@userGroupList'); // 用户分组列表(分组控制)
+			Route::match(['GET', 'POST'], 'add', 'GroupController@addUserGroup'); // 添加用户分组
+			Route::match(['GET', 'POST'], 'edit', 'GroupController@editUserGroup');// 编辑用户分组
+			Route::delete('delete', 'GroupController@delUserGroup'); // 删除用户分组
+		});
 	});
 
 	Route::get("payment/callbackList", "PaymentController@callbackList"); // 支付回调日志

+ 16 - 3
sql/db.sql

@@ -32,12 +32,13 @@ CREATE TABLE `ss_node`
     `server`         VARCHAR(255)         NULL     DEFAULT NULL COMMENT '服务器域名地址',
     `ip`             CHAR(15)             NULL     DEFAULT NULL COMMENT '服务器IPV4地址',
     `ipv6`           VARCHAR(45)          NULL     DEFAULT NULL COMMENT '服务器IPV6地址',
-    `relay_server`   VARCHAR(255)         NULL     DEFAULT NULL COMMENT '中转地址',
-    `relay_port`     SMALLINT(5) UNSIGNED NULL     DEFAULT 0 COMMENT '中转端口',
     `level`          TINYINT(3) UNSIGNED  NOT NULL DEFAULT '0' COMMENT '等级:0-无等级,全部可见',
     `speed_limit`    BIGINT(20) UNSIGNED  NOT NULL DEFAULT '0' COMMENT '节点限速,为0表示不限速,单位Byte',
     `client_limit`   SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '设备数限制',
+    `relay_server`   VARCHAR(255)         NULL     DEFAULT NULL COMMENT '中转地址',
+    `relay_port`     SMALLINT(5) UNSIGNED NULL     DEFAULT 0 COMMENT '中转端口',
     `description`    VARCHAR(255)         NULL     DEFAULT NULL COMMENT '节点简单描述',
+    `geo`            VARCHAR(255)         NULL     DEFAULT NULL COMMENT '节点地理位置',
     `method`         VARCHAR(32)          NOT NULL DEFAULT 'aes-256-cfb' COMMENT '加密方式',
     `protocol`       VARCHAR(64)          NOT NULL DEFAULT 'origin' COMMENT '协议',
     `protocol_param` VARCHAR(128)         NULL     DEFAULT NULL COMMENT '协议参数',
@@ -146,7 +147,7 @@ CREATE TABLE `user`
     `u`               BIGINT(20) UNSIGNED  NOT NULL DEFAULT '0' COMMENT '已上传流量,单位字节',
     `d`               BIGINT(20) UNSIGNED  NOT NULL DEFAULT '0' COMMENT '已下载流量,单位字节',
     `t`               INT(10) UNSIGNED     NOT NULL DEFAULT '0' COMMENT '最后使用时间',
-    `ip`              CHAR(15)                     DEFAULT NULL COMMENT '最后连接IP',
+    `ip`              CHAR(15)                      DEFAULT NULL COMMENT '最后连接IP',
     `enable`          TINYINT(1)           NOT NULL DEFAULT 1 COMMENT '代理状态',
     `method`          VARCHAR(30)          NOT NULL DEFAULT 'aes-256-cfb' COMMENT '加密方式',
     `protocol`        VARCHAR(30)          NOT NULL DEFAULT 'origin' COMMENT '协议',
@@ -161,6 +162,7 @@ CREATE TABLE `user`
     `ban_time`        INT(10) UNSIGNED     NOT NULL DEFAULT '0' COMMENT '封禁到期时间',
     `remark`          TEXT COMMENT '备注',
     `level`           TINYINT(3) UNSIGNED  NOT NULL DEFAULT '0' COMMENT '等级,默认0级',
+    `group_id`        INT(10) UNSIGNED     NOT NULL DEFAULT '0' COMMENT '所属分组',
     `is_admin`        BIT                  NOT NULL DEFAULT 0 COMMENT '是否管理员:0-否、1-是',
     `reg_ip`          CHAR(15)             NOT NULL DEFAULT '127.0.0.1' COMMENT '注册IP',
     `last_login`      INT(10) UNSIGNED     NOT NULL DEFAULT '0' COMMENT '最后登录时间',
@@ -185,6 +187,17 @@ VALUES (1, '管理员', 'test@test.com', '$2y$10$ryMdx5ejvCSdjvZVZAPpOuxHrsAUY8F
 UNLOCK TABLES;
 
 
+-- ----------------------------
+-- Records of `user_group`
+-- ----------------------------
+CREATE TABLE `user_group` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(255) DEFAULT NULL COMMENT '分组名称',
+  `nodes` text COMMENT '关联的节点ID,多个用,号分隔',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户分组控制表';
+
+
 -- ----------------------------
 -- Table structure for `level`
 -- ----------------------------

+ 13 - 0
sql/mod/20200725.sql

@@ -0,0 +1,13 @@
+ALTER TABLE `ss_node`
+    ADD `geo` VARCHAR(255) NULL DEFAULT NULL COMMENT '节点地理位置' AFTER `description`;
+
+ALTER TABLE `user`
+    ADD `group_id` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '所属分组' AFTER `level`;
+
+CREATE TABLE `user_group`
+(
+    `id`    INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+    `name`  VARCHAR(255) DEFAULT NULL COMMENT '分组名称',
+    `nodes` TEXT COMMENT '关联的节点ID,多个用,号分隔',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='用户分组控制表';

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff