198 Commits
1.5.2 ... 1.5.4

Author SHA1 Message Date
339ab3925f Merge pull request #492 from v2board/dev
1.5.4
2022-01-01 00:02:25 +08:00
927d575255 update: fix admin dark mode 2022-01-01 00:01:06 +08:00
796bcc4ab3 Merge pull request #491 from v2board/dev
update: fix admin dark mode
2021-12-31 23:56:12 +08:00
552a80f5f9 update: fix admin dark mode 2021-12-31 23:55:23 +08:00
d78a8a2a9f Merge pull request #490 from v2board/dev
1.5.4
2021-12-31 23:30:59 +08:00
3fad649377 update: cache version 2021-12-31 23:30:34 +08:00
389db6fb97 Merge pull request #489 from v2board/dev
1.5.4
2021-12-31 23:10:22 +08:00
080af12f39 Merge pull request #487 from Mr-Sheep/master
fix issue with surge subscribe
2021-12-31 23:09:57 +08:00
ecbdd12a6d Merge pull request #488 from betaxab/updaterules
rules: update clash & surge rules
2021-12-31 23:08:34 +08:00
3ade2bf89c update: code optimization 2021-12-31 01:49:27 +08:00
45a5022d82 update: logic optimization 2021-12-31 01:48:21 +08:00
e1a8d6ca35 update: logic optimization 2021-12-30 00:20:30 +08:00
5df2504f73 update: fix 2021-12-28 15:12:25 +08:00
708a83017b rules: update clash & surge rules
1. linkedin will be proxy
2. no longer use jsdelivr, avoid ruleset resources failing to load
3. update app.clash.yaml
2021-12-28 13:29:04 +08:00
edb2e7956c update: sql 2021-12-28 12:04:30 +08:00
a557aa6d32 update: add coupon limit period 2021-12-28 01:59:59 +08:00
6ed9cc559e update: more 2021-12-28 01:49:33 +08:00
6718a61890 update: support stash 2021-12-27 00:13:27 +08:00
333611da48 update: support stash 2021-12-27 00:05:28 +08:00
8ee7839d00 update: admin comission manage 2021-12-26 20:13:53 +08:00
e29261a87f update: get reset day 2021-12-26 19:35:24 +08:00
ba75d6e993 update: add send mass mail queue 2021-12-26 19:21:29 +08:00
ce8b5dde28 update: fix telegram markdown 2021-12-23 14:25:05 +08:00
55357a1b0e update: fix markdown parse 2021-12-17 13:35:54 +08:00
0f777f1a77 update: add telegram discuss link & ui 2021-12-17 01:53:03 +08:00
8fa73c2d4b update: remove favicon 2021-12-15 22:13:06 +08:00
80ae5f17b7 update: invite copy link 2021-12-15 21:35:01 +08:00
de4f01f653 update: invite copy link 2021-12-15 21:23:06 +08:00
bbfabdb72f update: add clash regex filter 2021-12-13 14:26:05 +08:00
b392fa3345 update: payment icon 2021-12-13 14:24:53 +08:00
ecfb9ce8b0 Fix Surge Subscribing issue 2021-12-11 21:24:27 +08:00
a69eb4058b update: ui & payment icons 2021-12-07 16:34:44 +08:00
8d56377c8a update: fix user commission show 2021-12-02 14:03:39 +08:00
30aec3d8e9 update: add test send mail 2021-11-30 16:23:46 +08:00
05769ea591 update: fix some bug 2021-11-24 13:33:42 +08:00
fb8bcdcbe0 update: fix user filter 2021-11-24 13:15:28 +08:00
39c47a06eb update: fix darkreader 2021-11-23 23:05:55 +08:00
4c71eb5633 update: fix dark mode qrcode 2021-11-23 01:42:00 +08:00
b8df411ffe update: fix darkrader 2021-11-22 03:20:52 +08:00
66728f13ea update: ui components language 2021-11-15 02:35:13 +08:00
7b1a8ee5ff update: add custom currency 2021-11-14 02:43:41 +08:00
9d96d68f12 update: reset traffic 2021-11-11 01:25:45 +08:00
d0947d1aaa update: fix coupon use 2021-11-01 02:31:27 +08:00
6f67096fe3 update: order callback no filter & save notice 2021-10-31 21:08:14 +08:00
44b369d0e7 update: fix subscribe loading 2021-10-28 14:59:48 +08:00
04fcd6758d update: fix v2ray config edit 2021-10-27 12:59:25 +08:00
99f3004d6b update: remove get subscribe id 2021-10-25 01:44:51 +08:00
56c726b173 update: fix payment update 2021-10-22 00:00:49 +08:00
37a6f3861c update: add payment save request validate 2021-10-18 17:10:40 +08:00
90211c1018 update: fix user filter 2021-10-16 20:47:27 +08:00
cc3e3b3fd1 update: fix background 2021-10-15 15:14:25 +08:00
10d3feb57c update: check order 2021-10-14 15:45:42 +08:00
eb98d706ae update: shell 2021-10-13 21:23:18 +08:00
4996c317c9 update: install 2021-10-13 21:04:23 +08:00
f380bd3c0e update: install 2021-10-13 21:04:02 +08:00
8e7f0bbc44 update: shell 2021-10-13 21:00:21 +08:00
53011cb95c Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-10-11 21:46:45 +08:00
037e22aa2d update: getInfo show uuid 2021-10-11 21:39:55 +08:00
ffd5eb269d Merge pull request #485 from betaxab/update-surfboard 2021-10-11 00:28:09 +09:00
68def9b4de update: fix filter 2021-10-10 01:07:58 +08:00
a4d617608f update: fix filter 2021-10-09 23:21:39 +08:00
364e259188 update: fix v2ray config drawer 2021-10-08 20:29:55 +08:00
b18c810f7f update: fix v2ray config drawer 2021-10-08 20:00:34 +08:00
239eb2075c Protocols: Surfboard: The SS can now be directly supported 2021-10-07 23:01:39 +08:00
b40272a8fa update: fix user generate 2021-10-06 00:20:28 +08:00
a6d5a433b0 update: composer 2021-10-05 21:03:27 +08:00
fe0f0afa53 update: dark mode 2021-10-04 23:10:11 +08:00
9ff524fd15 update: fix context menu 2021-10-04 21:45:17 +08:00
f6d9e6e7f6 update: fix v2ray network settings check 2021-10-04 14:37:32 +08:00
d911069e18 update: fix table context menu 2021-10-04 12:07:55 +08:00
32c539d2c0 Merge pull request #484 from v2board/dev
1.5.3
2021-10-03 22:19:10 +09:00
52fa1ce6af update: language 2021-10-01 20:18:40 +09:00
82d2d91582 update: user 2021-10-01 19:30:49 +09:00
8085c2ba6a update: schedule 2021-10-01 17:37:49 +09:00
b60fde5762 update: fix charts 2021-09-27 14:58:58 +09:00
3b51f12ab1 update: rollback app rule 2021-09-26 22:09:19 +09:00
ab4e66a5b6 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-09-26 22:07:34 +09:00
1856e0e87e update: fix support safari 15 theme color 2021-09-26 22:07:24 +09:00
e20ae29fb1 Merge pull request #483 from betaxab/fix-clash-rule-for-gp
rules: clash: Solve Google Play app download issue on China phones
2021-09-26 20:57:06 +09:00
cfd528739a update: fix en-us 2021-09-26 20:44:47 +09:00
dd23609658 update: change charts component 2021-09-25 00:33:11 +09:00
c7f3cf9e67 update: support safari theme 2021-09-24 01:50:58 +09:00
ddf2f45d6c update: user filter 2021-09-22 21:13:20 +09:00
ba3de90733 update: fix commission 2021-09-22 18:04:16 +09:00
43f4ecce93 update: fix 2021-09-21 22:30:00 +09:00
30b3587771 update: fix 2021-09-21 19:11:14 +09:00
6ab9a4d54d update: email notice default off 2021-09-21 19:07:53 +09:00
7a4bd468a2 update: add plan reset method 2021-09-21 18:51:53 +09:00
c97d37a070 rules: Solve Google Play app download issue on China phones
Update: app.clash.yaml up-to-date
2021-09-20 10:52:37 +08:00
25b3b11efd update: add commission distribution 2021-09-18 21:04:35 +09:00
edfc4043e8 update: add commission distribution 2021-09-18 20:53:34 +09:00
ab9abf5b93 update: crisp more data 2021-09-14 20:31:56 +09:00
6dd199631b update: crisp more data 2021-09-14 20:07:57 +09:00
4b863d681e update: crisp more data 2021-09-14 19:47:57 +09:00
5d6010045d update: support md5 with sha256 2021-09-14 13:12:44 +09:00
0374a03892 update: support md5 with sha256 2021-09-14 13:10:29 +09:00
ec00fc4496 update: support md5 with sha256 2021-09-14 02:38:24 +09:00
a365357770 update: fix stat 2021-09-07 12:49:35 +09:00
14579d1eea update: update alert 2021-09-07 03:49:04 +09:00
9f6d1ada93 update: v2board install 2021-09-07 03:18:45 +09:00
895976b830 update: horizon config 2021-09-07 01:47:29 +09:00
4b011225db Merge pull request #482 from betaxab/subs-domain-force-direct 2021-09-07 01:33:37 +09:00
243aed3f55 Merge pull request #480 from betaxab/horizon-metrics 2021-09-07 01:33:24 +09:00
d25d7ba69b Merge pull request #479 from betaxab/update 2021-09-07 01:33:16 +09:00
c0c84efb7c Protocols: force the current subscription domain to be a direct rule 2021-09-06 18:26:03 +08:00
91c7dfc0ed update: add no reset method 2021-09-06 02:31:45 +09:00
6830e6af38 update: push tag 2021-09-06 01:24:41 +09:00
cb75579772 update: fix push tag 2021-09-06 01:19:43 +09:00
e980c2d8f3 update: set horizon with mem 2021-09-05 18:05:25 +09:00
b6195494d3 Kernel: add horizon snapshot schedule 2021-09-05 10:32:33 +08:00
ccf3497241 update: register api 2021-09-04 21:41:57 +09:00
c80d93fa25 update: order queue 2021-09-04 16:31:25 +09:00
c2577e37c4 update: server log 2021-09-03 00:31:12 +09:00
c183462ef3 update: server log 2021-09-02 21:09:04 +09:00
2a5e9ef079 update: rollback 2021-09-02 20:28:24 +09:00
decbae1413 update: traffic fetch job 2021-09-02 19:58:04 +09:00
0c14652ff7 update: server log fetch 2021-09-02 18:42:36 +09:00
91418caf04 update: log record 2021-09-01 03:32:15 +09:00
607aa82f88 update: horizon config 2021-09-01 03:08:31 +09:00
adc2d02c49 update: horizon config 2021-09-01 03:08:07 +09:00
fabb49baea update: horizon auth 2021-09-01 02:11:19 +09:00
35e11a6816 update: add horizon 2021-09-01 01:56:16 +09:00
1d87a1b99a update: traffic fetch queue 2021-09-01 00:55:07 +09:00
fbced4d09b update: fix order assign modal 2021-08-31 20:59:56 +09:00
52914e354e update: order assign notice 2021-08-31 20:54:08 +09:00
d392d29b50 update: reset text 2021-08-30 19:11:16 +09:00
347a3bb4b0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-30 01:18:17 +09:00
7ac1f69a71 update: fix css 2021-08-30 01:18:06 +09:00
6317c4a4f3 Protocols: Clash: update 2021-08-29 21:44:51 +08:00
3da4de02d0 Merge pull request #478 from betaxab/fix-clash-appname
Protocols: Clash: fix Clash app_name not fould
2021-08-29 22:38:38 +09:00
85686df2e6 Protocols: Clash: fix Clash app_name not found 2021-08-29 21:32:46 +08:00
adb5d041e6 fix: remind expire 2021-08-29 12:56:55 +09:00
7ba0e9a4e0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:55:08 +09:00
99a64f41e1 Merge pull request #477 from betaxab/abandon-clash-provider
Protocols: Clash: Rollback the previous config
2021-08-29 12:54:58 +09:00
62053bc30f Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:54:32 +09:00
e5e7a06514 update: fix coupon 2021-08-29 12:54:15 +09:00
905e2dacd1 Protocols: Clash: Rollback the previous configuration
because the provider cannot test the delay manually
2021-08-29 11:20:55 +08:00
8189a442bc Merge pull request #475 from WayChan/update_clash_subscribe
update Clash Subscribe
2021-08-29 02:52:51 +09:00
b38f7979d6 update: coupon 2021-08-28 16:34:22 +09:00
5a3b897c57 update: add coupon per user limit 2021-08-28 16:32:55 +09:00
2d5fb03937 update: remind mail i18n 2021-08-28 15:39:10 +09:00
982c47d0b4 update: remind mail i18n 2021-08-28 15:32:38 +09:00
4cebeed2d7 update: server log 2021-08-28 15:22:51 +09:00
c95d374cc0 update: order manual 2021-08-28 15:11:59 +09:00
23462e2753 update: add more tips 2021-08-27 00:54:35 +09:00
b26a3e0e70 update: fix order update 2021-08-25 18:06:08 +09:00
3600c9a166 update: rollback traffic fetch 2021-08-24 16:53:53 +09:00
8a58a1ad88 update 2021-08-24 16:50:45 +09:00
968d55e2e3 update: readme 2021-08-22 15:46:49 +09:00
98ac5cb680 Fix Clash Json decode error 2021-08-22 00:09:37 +08:00
0288d2df4b update: order success handle 2021-08-21 20:52:00 +09:00
cb8e34fa2e change profile-update-interval time 2021-08-21 17:59:58 +08:00
b0c818c661 update 2021-08-21 16:20:16 +09:00
303d4a1c66 update: short payment generate key 2021-08-20 23:42:07 +09:00
e6416712f0 Merge pull request #471 from betaxab/add-trojan-surfboard
Protocols: Surfboard: add trojan protocol support
2021-08-20 22:32:08 +09:00
5c6236366c Merge pull request #472 from betaxab/change-anxray-flag
Protocols: AnXray: change AnXray flag to axxray
2021-08-20 22:31:49 +09:00
9b0a487c69 update: add knowledge jump func 2021-08-20 22:30:18 +09:00
27a6cc98d1 update Clash Subscribe 2021-08-20 00:54:00 +08:00
e05f6116b6 Protocols: AnXray: change AnXray flag to axxray 2021-08-19 15:47:18 +08:00
a9db11877f Protocols: Surfboard: add trojan protocol support 2021-08-19 08:10:36 +08:00
abe1ebccae update: new generate order number method 2021-08-19 00:58:51 +09:00
c957a4ca83 update: fix ticket message length support utf8mb4 2021-08-18 21:23:45 +09:00
c9cd307cc4 update: auto open knowledge by id 2021-08-18 18:54:40 +09:00
31cdcef3aa update: remove apple id config 2021-08-12 17:28:15 +09:00
9f75d4cbde update: default theme add green color 2021-08-12 17:17:36 +09:00
de5f80b5a3 update: script 2021-08-10 13:37:37 +09:00
474df5e18f update: payment config format 2021-08-08 16:56:50 +09:00
dfa75f49bd update: fix tg 2021-08-07 19:00:33 +09:00
f2c7d092ac update: fix model 2021-08-06 23:41:41 +09:00
2c389ebe8c update: v2board update 2021-08-06 23:20:10 +09:00
25e19446e6 update: user tags 2021-08-06 17:33:16 +09:00
a8761e9d4d update: opt editor 2021-08-06 14:02:47 +09:00
5f4e9c0301 update: cache version 2021-08-06 13:51:55 +09:00
b6e9260464 update: fix editor 2021-08-06 13:51:30 +09:00
6aa96fe856 update: fix editor 2021-08-06 13:01:17 +09:00
ab02935fd7 update: fix v2board udpate 2021-08-06 02:40:31 +09:00
b275ad469b update: cache version 2021-08-06 01:53:51 +09:00
e6a7c2c11c update: opt code 2021-08-06 01:43:01 +09:00
0f0f726269 update: fix code support php8 2021-08-05 15:22:15 +09:00
00cd3e26be update: fix admin editor 2021-08-05 14:48:24 +09:00
d95974019a update: fix 2021-08-02 03:00:01 +09:00
7a80950ab5 update: laravel 7 2021-08-01 23:56:11 +09:00
73a6d3236a update: fix reset traffic maybe failure issue 2021-08-01 16:48:00 +09:00
59dd34674e update: rollback ui 2021-07-31 16:24:12 +09:00
5dda531c2b update: ui 2021-07-31 15:15:05 +09:00
7234ccf4c1 update: ui 2021-07-31 14:17:48 +09:00
448b5382b9 Merge pull request #468 from betaxab/fix-grpc-protocol 2021-07-31 13:17:06 +09:00
bd2b056fbf protocols: fix gRPC protocol serviceName field
fix anXray serverName filed
2021-07-31 12:14:40 +08:00
ad8e2b8e80 update: api 2021-07-31 03:36:49 +09:00
8a20a70513 update: fix ui 2021-07-31 02:34:58 +09:00
06adaa2124 update: fix order statistics 2021-07-31 02:21:49 +09:00
ebf98d42a8 update: new feature 2021-07-31 02:05:39 +09:00
1adb1bcfa0 update: fix default app_url with subscribe_url 2021-07-30 23:53:37 +09:00
bb8da6e2c5 update: fix payment verify app_url 2021-07-30 16:00:20 +09:00
c426aabbcf update: theme 2021-07-29 21:29:53 +09:00
18bb1bd962 update: theme 2021-07-29 21:24:37 +09:00
07e9377417 update: theme 2021-07-29 21:22:07 +09:00
c0d3150461 update: add payment app_url alert 2021-07-29 21:12:11 +09:00
143 changed files with 4429 additions and 954 deletions

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Models\CommissionLog;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
@ -59,27 +60,66 @@ class CheckCommission extends Command
public function autoPayCommission()
{
$order = Order::where('commission_status', 1)
$orders = Order::where('commission_status', 1)
->where('invite_user_id', '!=', NULL)
->get();
foreach ($order as $item) {
$inviter = User::find($item->invite_user_id);
if (!$inviter) continue;
if ((int)config('v2board.withdraw_close_enable', 0)) {
$inviter->balance = $inviter->balance + $item->commission_balance;
} else {
$inviter->commission_balance = $inviter->commission_balance + $item->commission_balance;
}
foreach ($orders as $order) {
DB::beginTransaction();
if ($inviter->save()) {
$item->commission_status = 2;
if (!$item->save()) {
DB::rollBack();
continue;
}
DB::commit();
if (!$this->payHandle($order->invite_user_id, $order)) {
DB::rollBack();
continue;
}
$order->commission_status = 2;
if (!$order->save()) {
DB::rollBack();
continue;
}
DB::commit();
}
}
public function payHandle($inviteUserId, Order $order)
{
if ((int)config('v2board.commission_distribution_enable', 0)) {
$level = 3;
$commissionShareLevels = [
0 => (int)config('v2board.commission_distribution_l1'),
1 => (int)config('v2board.commission_distribution_l2'),
2 => (int)config('v2board.commission_distribution_l3')
];
} else {
$level = 3;
$commissionShareLevels = [
0 => 100
];
}
for ($l = 0; $l < $level; $l++) {
$inviter = User::find($inviteUserId);
if (!$inviter) continue;
if (!isset($commissionShareLevels[$l])) continue;
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
if ((int)config('v2board.withdraw_close_enable', 0)) {
$inviter->balance = $inviter->balance + $commissionBalance;
} else {
$inviter->commission_balance = $inviter->commission_balance + $commissionBalance;
}
if (!$inviter->save()) {
DB::rollBack();
return false;
}
if (!CommissionLog::create([
'invite_user_id' => $inviteUserId,
'user_id' => $order->user_id,
'trade_no' => $order->trade_no,
'order_amount' => $order->total_amount,
'get_amount' => $commissionBalance
])) {
DB::rollBack();
return false;
}
$inviteUserId = $inviter->invite_user_id;
}
return true;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Jobs\OrderHandleJob;
use App\Services\OrderService;
use Illuminate\Console\Command;
use App\Models\Order;
@ -44,21 +45,10 @@ class CheckOrder extends Command
{
ini_set('memory_limit', -1);
$orders = Order::whereIn('status', [0, 1])
->orderBy('created_at', 'ASC')
->get();
foreach ($orders as $item) {
$orderService = new OrderService($item);
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$orderService->cancel();
}
break;
case 1:
$orderService->open();
break;
}
foreach ($orders as $order) {
OrderHandleJob::dispatch($order->trade_no);
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Models\Plan;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
@ -42,24 +43,45 @@ class ResetTraffic extends Command
*/
public function handle()
{
DB::beginTransaction();
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// 1 a month
case 0:
$this->resetByMonthFirstDay();
break;
// expire day
case 1:
$this->resetByExpireDay();
break;
ini_set('memory_limit', -1);
foreach (Plan::get() as $plan) {
switch ($plan->reset_traffic_method) {
case null: {
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($this->builder);
break;
// expire day
case 1:
$this->resetByExpireDay($this->builder);
break;
// no action
case 2:
break;
}
break;
}
case 0: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByMonthFirstDay($builder);
break;
}
case 1: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByExpireDay($builder);
break;
}
case 2: {
break;
}
}
}
DB::commit();
}
private function resetByMonthFirstDay():void
private function resetByMonthFirstDay($builder):void
{
$builder = $this->builder;
if ((string)date('d') === '01') {
$builder->update([
'u' => 0,
@ -68,9 +90,8 @@ class ResetTraffic extends Command
}
}
private function resetByExpireDay():void
private function resetByExpireDay($builder):void
{
$builder = $this->builder;
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($builder->get() as $item) {

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Services\MailService;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
@ -41,23 +42,9 @@ class SendRemindMail extends Command
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $this->remindExpire($user);
}
}
private function remindExpire($user)
{
if ($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time()) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的服务即将到期',
'template_name' => 'remindExpire',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
if ($user->remind_expire) $mailService->remindExpire($user);
}
}
}

View File

@ -3,7 +3,12 @@
namespace App\Console\Commands;
use App\Models\Order;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Matriphe\Larinfo;
class Test extends Command
{

View File

@ -47,13 +47,12 @@ class V2boardInstall extends Command
$this->info(" \ \ / / __) | _ \ / _ \ / _` | '__/ _` | ");
$this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | ");
$this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| ");
if (\File::exists(base_path() . '/.lock')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.lock文件');
if (\File::exists(base_path() . '/.env')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.env文件');
}
if (!\File::exists(base_path() . '/.env')) {
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
$this->saveToEnv([
'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')),
@ -100,7 +99,6 @@ class V2boardInstall extends Command
$this->info('一切就绪');
$this->info('访问 http(s)://你的站点/admin 进入管理面板');
\File::put(base_path() . '/.lock', time());
} catch (\Exception $e) {
$this->error($e->getMessage());
}

View File

@ -42,16 +42,16 @@ class V2boardStatistics extends Command
*/
public function handle()
{
$this->statOrder();
$this->statServer();
$this->statOrder();
$this->statServer();
}
private function statOrder()
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$builder = Order::where('created_at', '>=', $startAt)
->where('created_at', '<', $endAt)
$builder = Order::where('paid_at', '>=', $startAt)
->where('paid_at', '<', $endAt)
->whereNotIn('status', [0, 2]);
$orderCount = $builder->count();
$orderAmount = $builder->sum('total_amount');

View File

@ -51,11 +51,12 @@ class V2boardUpdate extends Command
}
$this->info('正在导入数据库请稍等...');
foreach ($sql as $item) {
if (!$item) continue;
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {
}
}
$this->info('更新完毕');
$this->info('更新完毕,请重新启动队列服务。');
}
}

4
app/Console/Kernel.php Executable file → Normal file
View File

@ -31,9 +31,11 @@ class Kernel extends ConsoleKernel
$schedule->command('check:commission')->everyMinute();
// reset
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->quarterly();
$schedule->command('reset:serverLog')->quarterly()->at('0:15');
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
/**

View File

@ -2,8 +2,8 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
@ -29,10 +29,12 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* @param \Exception $exception
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Exception $exception)
public function report(Throwable $exception)
{
parent::report($exception);
}
@ -40,16 +42,14 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Exception $exception)
public function render($request, Throwable $exception)
{
if($exception instanceof \Illuminate\Http\Exceptions\ThrottleRequestsException) {
abort(429, '请求频繁,请稍后再试');
}
return parent::render($request, $exception);
}
}

View File

@ -3,10 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Mail;
class ConfigController extends Controller
{
@ -32,6 +34,24 @@ class ConfigController extends Controller
]);
}
public function testSendMail(Request $request)
{
$obj = new SendEmailJob([
'email' => $request->session()->get('email'),
'subject' => 'This is v2board test email',
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'content' => 'This is v2board test email',
'url' => config('v2board.app_url')
]
]);
return response([
'data' => true,
'log' => $obj->handle()
]);
}
public function setTelegramWebhook(Request $request)
{
$telegramService = new TelegramService($request->input('telegram_bot_token'));
@ -60,7 +80,11 @@ class ConfigController extends Controller
'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1),
'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100),
'commission_withdraw_method' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0)
'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0),
'commission_distribution_enable' => config('v2board.commission_distribution_enable', 0),
'commission_distribution_l1' => config('v2board.commission_distribution_l1'),
'commission_distribution_l2' => config('v2board.commission_distribution_l2'),
'commission_distribution_l3' => config('v2board.commission_distribution_l3')
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
@ -78,7 +102,9 @@ class ConfigController extends Controller
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'tos_url' => config('v2board.tos_url')
'tos_url' => config('v2board.tos_url'),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
],
'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
@ -136,9 +162,6 @@ class ConfigController extends Controller
'server_v2ray_domain' => config('v2board.server_v2ray_domain'),
'server_v2ray_protocol' => config('v2board.server_v2ray_protocol'),
],
'tutorial' => [
'apple_id' => config('v2board.apple_id')
],
'email' => [
'email_template' => config('v2board.email_template', 'default'),
'email_host' => config('v2board.email_host'),
@ -150,7 +173,8 @@ class ConfigController extends Controller
],
'telegram' => [
'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
'telegram_bot_token' => config('v2board.telegram_bot_token')
'telegram_bot_token' => config('v2board.telegram_bot_token'),
'telegram_discuss_link' => config('v2board.telegram_discuss_link')
],
'app' => [
'windows_version' => config('v2board.windows_version'),
@ -166,7 +190,7 @@ class ConfigController extends Controller
public function save(ConfigSave $request)
{
$data = $request->input();
$data = $request->validated();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, array_keys($request->validated()))) {

View File

@ -19,47 +19,17 @@ class CouponController extends Controller
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$sort = $request->input('sort') ? $request->input('sort') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
foreach ($coupons as $k => $v) {
if ($coupons[$k]['limit_plan_ids']) $coupons[$k]['limit_plan_ids'] = json_decode($coupons[$k]['limit_plan_ids']);
}
return response([
'data' => $coupons,
'total' => $total
]);
}
public function save(CouponSave $request)
{
$params = $request->validated();
if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
}
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function generate(CouponGenerate $request)
{
if ($request->input('generate_count')) {
@ -68,9 +38,6 @@ class CouponController extends Controller
}
$params = $request->validated();
if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
}
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
@ -95,10 +62,9 @@ class CouponController extends Controller
{
$coupons = [];
$coupon = $request->validated();
if (isset($coupon['limit_plan_ids'])) {
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
}
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
$coupon['limit_period'] = json_encode($coupon['limit_period']);
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
@ -118,7 +84,7 @@ class CouponController extends Controller
$endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
$limitUse = $coupon['limit_use'] ?? '不限制';
$createTime = date('Y-m-d H:i:s', $coupon['created_at']);
$limitPlanIds = $coupon['limit_plan_ids'] ?? '不限制';
$limitPlanIds = implode("/", json_decode($coupon['limit_plan_ids'], true)) ?? '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;

View File

@ -6,6 +6,7 @@ use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Http\Requests\Admin\OrderFetch;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@ -63,10 +64,45 @@ class OrderController extends Controller
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'status',
'commission_status'
]);
@ -76,16 +112,6 @@ class OrderController extends Controller
abort(500, '订单不存在');
}
if (isset($params['status']) && (int)$params['status'] === 2) {
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
try {
$order->update($params);
} catch (\Exception $e) {
@ -97,26 +123,6 @@ class OrderController extends Controller
]);
}
public function repair(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数错误');
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或订单已支付');
}
$order->status = 1;
if (!$order->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
@ -130,16 +136,21 @@ class OrderController extends Controller
abort(500, '该订阅不存在');
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
abort(500, '该用户还有待支付的订单,无法分配');
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->cycle = $request->input('cycle');
$order->period = $request->input('period');
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->cycle === 'reset_price') {
if ($order->period === 'reset_price') {
$order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = 3;

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\PaymentSave;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
@ -42,6 +43,9 @@ class PaymentController extends Controller
public function save(Request $request)
{
if (!config('v2board.app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
@ -54,11 +58,21 @@ class PaymentController extends Controller
'data' => true
]);
}
$request->validate([
'name' => 'required',
'payment' => 'required',
'config' => 'required'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空'
]);
if (!Payment::create([
'name' => $request->input('name'),
'icon' => $request->input('icon'),
'payment' => $request->input('payment'),
'config' => $request->input('config'),
'uuid' => Helper::guid()
'uuid' => Helper::randomChar(8)
])) {
abort(500, '保存失败');
}

View File

@ -3,9 +3,12 @@
namespace App\Http\Controllers\Admin\Server;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerV2ray;
use App\Models\ServerGroup;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@ -18,8 +21,20 @@ class GroupController extends Controller
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
$serverGroups = ServerGroup::get();
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0;
foreach ($servers as $server) {
if (in_array($v['id'], $server['group_id'])) {
$serverGroups[$k]['server_count'] = $serverGroups[$k]['server_count']+1;
}
}
}
return response([
'data' => ServerGroup::get()
'data' => $serverGroups
]);
}
@ -50,10 +65,9 @@ class GroupController extends Controller
}
}
$servers = Server::all();
$servers = ServerV2ray::all();
foreach ($servers as $server) {
$groupId = json_decode($server->group_id);
if (in_array($request->input('id'), $groupId)) {
if (in_array($request->input('id'), $server->group_id)) {
abort(500, '该组已被节点所使用,无法删除');
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers\Admin\Server;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Services\ServerService;
@ -22,6 +22,7 @@ class ManageController extends Controller
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
DB::beginTransaction();
foreach ($request->input('sorts') as $k => $v) {
switch ($v['key']) {
@ -32,7 +33,7 @@ class ManageController extends Controller
}
break;
case 'v2ray':
if (!Server::find($v['value'])->update(['sort' => $k + 1])) {
if (!ServerV2ray::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}

View File

@ -14,11 +14,6 @@ class ShadowsocksController extends Controller
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {

View File

@ -14,11 +14,6 @@ class TrojanController extends Controller
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {

View File

@ -7,44 +7,16 @@ use App\Http\Requests\Admin\ServerV2rayUpdate;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\ServerV2ray;
class V2rayController extends Controller
{
public function save(ServerV2raySave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if (isset($params['dnsSettings'])) {
if (!is_object(json_decode($params['dnsSettings']))) {
abort(500, 'DNS规则配置格式不正确');
}
}
if (isset($params['ruleSettings'])) {
if (!is_object(json_decode($params['ruleSettings']))) {
abort(500, '审计规则配置格式不正确');
}
}
if (isset($params['networkSettings'])) {
if (!is_object(json_decode($params['networkSettings']))) {
abort(500, '传输协议配置格式不正确');
}
}
if (isset($params['tlsSettings'])) {
if (!is_object(json_decode($params['tlsSettings']))) {
abort(500, 'TLS配置格式不正确');
}
}
if ($request->input('id')) {
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
@ -58,7 +30,7 @@ class V2rayController extends Controller
]);
}
if (!Server::create($params)) {
if (!ServerV2ray::create($params)) {
abort(500, '创建失败');
}
@ -70,7 +42,7 @@ class V2rayController extends Controller
public function drop(Request $request)
{
if ($request->input('id')) {
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
@ -86,7 +58,7 @@ class V2rayController extends Controller
'show',
]);
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
@ -104,12 +76,12 @@ class V2rayController extends Controller
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!Server::create($server->toArray())) {
if (!ServerV2ray::create($server->toArray())) {
abort(500, '复制失败');
}

View File

@ -8,7 +8,7 @@ use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerGroup;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Plan;
use App\Models\User;
use App\Models\Ticket;
@ -91,7 +91,7 @@ class StatController extends Controller
{
$servers = [
'shadowsocks' => ServerShadowsocks::where('parent_id', null)->get()->toArray(),
'vmess' => Server::where('parent_id', null)->get()->toArray(),
'vmess' => ServerV2ray::where('parent_id', null)->get()->toArray(),
'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray()
];
$timestamp = strtotime('-1 day', strtotime(date('Y-m-d')));

View File

@ -30,20 +30,22 @@ class UserController extends Controller
private function filter(Request $request, $builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['value'])->first();
if (!$user) continue;
$builder->where('invite_user_id', $user->id);
continue;
$filters = $request->input('filter');
if ($filters) {
foreach ($filters as $k => $filter) {
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['condition'], $filter['value'])->first();
$inviteUserId = isset($user->id) ? $user->id : 0;
$builder->where('invite_user_id', $inviteUserId);
unset($filters[$k]);
continue;
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
@ -68,7 +70,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
$res[$i]['subscribe_url'] = Helper::getSubscribeHost() . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
}
return response([
'data' => $res,
@ -179,6 +181,9 @@ class UserController extends Controller
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
abort(500, '邮箱已存在于系统中');
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
abort(500, '生成失败');
@ -251,7 +256,8 @@ class UserController extends Controller
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
],
'send_email_mass');
}
return response([

View File

@ -7,7 +7,7 @@ use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Clash;
use Illuminate\Http\Request;
use App\Models\Server;
use App\Models\ServerV2ray;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;

View File

@ -4,7 +4,7 @@ namespace App\Http\Controllers\Client\Protocols;
class AnXray
{
public $flag = 'anxray';
public $flag = 'axxray';
private $servers;
private $user;
@ -64,15 +64,21 @@ class AnXray
"encryption" => "none",
"type" => urlencode($server['network']),
"security" => $server['tls'] ? "tls" : "",
"sni" => $server['tls'] ? urlencode(json_decode($server['tlsSettings'], true)['serverName']) : ""
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = urlencode($tlsSettings['serverName']);
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = urlencode($wsSettings['path']);
if (isset($wsSettings['headers']['Host'])) $config['host'] = urlencode($wsSettings['headers']['Host']);
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = json_decode($server['networkSettings'], true);
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['serviceName'] = urlencode($grpcSettings['serviceName']);
}
return "vmess://" . $uuid . "@" . $server['host'] . ":" . $server['port'] . "?" . http_build_query($config) . "#" . urlencode($server['name']) . "\r\n";

View File

@ -20,7 +20,10 @@ class Clash
{
$servers = $this->servers;
$user = $this->user;
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition: filename={$appName}");
$defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.clash.yaml';
if (\File::exists($customConfig)) {
@ -49,8 +52,25 @@ class Clash
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
if (!is_array($config['proxy-groups'][$k]['proxies'])) continue;
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
$isFilter = false;
foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
foreach ($proxies as $dst) {
if ($this->isMatch($src, $dst)) {
$isFilter = true;
$config['proxy-groups'][$k]['proxies'] = array_diff($config['proxy-groups'][$k]['proxies'], [$src]);
array_push($config['proxy-groups'][$k]['proxies'], $dst);
}
}
}
if (!$isFilter) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
}
// Force the current subscription domain to be a direct rule
$subsDomain = $_SERVER['SERVER_NAME'];
$subsDomainRule = "DOMAIN,{$subsDomain},DIRECT";
array_unshift($config['rules'], $subsDomainRule);
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
@ -84,7 +104,7 @@ class Clash
if ($server['tls']) {
$array['tls'] = true;
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
@ -94,7 +114,7 @@ class Clash
if ($server['network'] === 'ws') {
$array['network'] = 'ws';
if ($server['networkSettings']) {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
@ -104,7 +124,7 @@ class Clash
if ($server['network'] === 'grpc') {
$array['network'] = 'grpc';
if ($server['networkSettings']) {
$grpcObject = json_decode($server['networkSettings'], true);
$grpcObject = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcObject['serviceName'];
}
@ -126,4 +146,13 @@ class Clash
if (!empty($server['allow_insecure'])) $array['skip-cert-verify'] = ($server['allow_insecure'] ? true : false);
return $array;
}
private function isMatch($exp, $str)
{
try {
return preg_match($exp, $str);
} catch (\Exception $e) {
return false;
}
}
}

View File

@ -63,19 +63,19 @@ class Passwall
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = json_decode($server['networkSettings'], true);
if (isset($grpcSettings['path'])) $config['path'] = $grpcSettings['serviceName'];
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}

View File

@ -66,7 +66,7 @@ class QuantumultX
if ($server['network'] === 'tcp')
array_push($config, 'obfs=over-tls');
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'tls-verification=' . ($tlsSettings['allowInsecure'] ? 'false' : 'true'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
@ -79,7 +79,7 @@ class QuantumultX
else
array_push($config, 'obfs=ws');
if ($server['networkSettings']) {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "obfs-uri={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']) && !isset($host))

View File

@ -63,19 +63,19 @@ class SSRPlus
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = json_decode($server['networkSettings'], true);
if (isset($grpcSettings['path'])) $config['path'] = $grpcSettings['serviceName'];
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}

View File

@ -63,7 +63,7 @@ class Shadowrocket
if ($server['tls']) {
$config['tls'] = 1;
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$config['allowInsecure'] = (int)$tlsSettings['allowInsecure'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
@ -73,7 +73,7 @@ class Shadowrocket
if ($server['network'] === 'ws') {
$config['obfs'] = "websocket";
if ($server['networkSettings']) {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
@ -83,7 +83,7 @@ class Shadowrocket
if ($server['network'] === 'grpc') {
$config['obfs'] = "grpc";
if ($server['networkSettings']) {
$grpcSettings = json_decode($server['networkSettings'], true);
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName']) && !empty($grpcSettings['serviceName']))
$config['path'] = $grpcSettings['serviceName'];
}

View File

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
use Symfony\Component\Yaml\Yaml;
class Stash
{
public $flag = 'stash';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition: filename={$appName}");
// 暂时使用clash配置文件后续根据Stash更新情况更新
$defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.clash.yaml';
if (\File::exists($customConfig)) {
$config = Yaml::parseFile($customConfig);
} else {
$config = Yaml::parseFile($defaultConfig);
}
$proxy = [];
$proxies = [];
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, self::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'v2ray') {
array_push($proxy, self::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, self::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']);
}
}
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
if (!is_array($config['proxy-groups'][$k]['proxies'])) continue;
$isFilter = false;
foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
foreach ($proxies as $dst) {
if ($this->isMatch($src, $dst)) {
$isFilter = true;
$config['proxy-groups'][$k]['proxies'] = array_diff($config['proxy-groups'][$k]['proxies'], [$src]);
array_push($config['proxy-groups'][$k]['proxies'], $dst);
}
}
}
if (!$isFilter) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
}
// Force the current subscription domain to be a direct rule
$subsDomain = $_SERVER['SERVER_NAME'];
$subsDomainRule = "DOMAIN,{$subsDomain},DIRECT";
array_unshift($config['rules'], $subsDomainRule);
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
public static function buildShadowsocks($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'ss';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['cipher'] = $server['cipher'];
$array['password'] = $uuid;
$array['udp'] = true;
return $array;
}
public static function buildVmess($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'vmess';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['uuid'] = $uuid;
$array['alterId'] = $server['alter_id'];
$array['cipher'] = 'auto';
$array['udp'] = true;
if ($server['tls']) {
$array['tls'] = true;
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$array['servername'] = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
$array['network'] = 'ws';
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']];
}
}
if ($server['network'] === 'grpc') {
$array['network'] = 'grpc';
if ($server['networkSettings']) {
$grpcObject = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcObject['serviceName'];
}
}
return $array;
}
public static function buildTrojan($password, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'trojan';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['password'] = $password;
$array['udp'] = true;
if (!empty($server['server_name'])) $array['sni'] = $server['server_name'];
if (!empty($server['allow_insecure'])) $array['skip-cert-verify'] = ($server['allow_insecure'] ? true : false);
return $array;
}
private function isMatch($exp, $str)
{
try {
return preg_match($exp, $str);
} catch (\Exception $e) {
return false;
}
}
}

View File

@ -26,13 +26,19 @@ class Surfboard
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= Surfboard::buildShadowsocks($user['uuid'], $item);
$proxies .= self::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= Surfboard::buildVmess($user['uuid'], $item);
$proxies .= self::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [Proxy]
$proxies .= self::buildTrojan($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
@ -48,8 +54,10 @@ class Surfboard
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$subsDomain = $_SERVER['SERVER_NAME'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
@ -59,12 +67,11 @@ class Surfboard
public static function buildShadowsocks($password, $server)
{
$config = [
"{$server['name']}=custom",
"{$server['name']}=ss",
"{$server['host']}",
"{$server['port']}",
"{$server['cipher']}",
"{$password}",
'https://raw.githubusercontent.com/Hackl0us/proxy-tool-backup/master/SSEncrypt.module',
"encrypt-method={$server['cipher']}",
"password={$password}",
'tfo=true',
'udp-relay=true'
];
@ -88,7 +95,7 @@ class Surfboard
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
@ -98,7 +105,7 @@ class Surfboard
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
@ -110,4 +117,24 @@ class Surfboard
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"{$server['name']}=trojan",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
$server['server_name'] ? "sni={$server['server_name']}" : "",
'tfo=true',
'udp-relay=true'
];
if (!empty($server['allow_insecure'])) {
array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
}
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -52,9 +52,11 @@ class Surge
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$subsDomain = $_SERVER['SERVER_NAME'];
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
@ -92,7 +94,7 @@ class Surge
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
@ -102,7 +104,7 @@ class Surge
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))

View File

@ -63,19 +63,19 @@ class V2rayN
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = json_decode($server['networkSettings'], true);
if (isset($grpcSettings['path'])) $config['path'] = $grpcSettings['serviceName'];
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}

View File

@ -63,19 +63,19 @@ class V2rayNG
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = json_decode($server['tlsSettings'], true);
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = json_decode($server['networkSettings'], true);
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = json_decode($server['networkSettings'], true);
if (isset($grpcSettings['path'])) $config['path'] = $grpcSettings['serviceName'];
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}

View File

@ -34,7 +34,7 @@ class PaymentController extends Controller
}
if ($order->status === 1) return true;
$orderService = new OrderService($order);
if (!$orderService->success($callbackNo)) {
if (!$orderService->paid($callbackNo)) {
return false;
}
$telegramService = new TelegramService();

View File

@ -102,10 +102,15 @@ class AuthController extends Controller
if ((int)config('v2board.email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$data = [
'token' => $user->token,
'auth_data' => base64_encode("{$user->email}:{$user->password}")
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
return response()->json([
'data' => true
'data' => $data
]);
}
@ -120,6 +125,7 @@ class AuthController extends Controller
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
@ -250,6 +256,7 @@ class AuthController extends Controller
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, __('Reset failed'));
}

View File

@ -8,7 +8,7 @@ use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -35,13 +35,13 @@ class DeepbworkController extends Controller
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
$server = ServerV2ray::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
@ -64,7 +64,7 @@ class DeepbworkController extends Controller
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
$server = ServerV2ray::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
@ -76,23 +76,11 @@ class DeepbworkController extends Controller
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess');
}
DB::commit();
return response([
'ret' => 1,

View File

@ -1,158 +0,0 @@
<?php
namespace App\Http\Controllers\Server;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Poseidon
* Github: https://github.com/ColetteContreras/trojan-poseidon
*/
class PoseidonController extends Controller
{
public $poseidonVersion;
public function __construct(Request $request)
{
$this->poseidonVersion = $request->input('poseidon_version');
}
// 后端获取用户
public function user(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
if (!$server) {
return $this->error("server could not be found", 404);
}
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $server->alter_id,
"level" => 0,
];
unset($user['uuid']);
unset($user['email']);
array_push($result, $user);
}
return $this->success($result);
}
// 后端提交数据
public function submit(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$server = Server::find($request->input('node_id'));
if (!$server) {
return $this->error("server could not be found", 404);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess')) {
return $this->error("user fetch fail", 500);
}
}
return $this->success('');
}
// 后端获取配置
public function config(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
return $this->error('invalid parameters', 400);
}
$serverService = new ServerService();
try {
$json = $serverService->getV2RayConfig($nodeId, $localPort);
$json->poseidon = [
'license_key' => (string)config('v2board.server_license'),
];
if ($this->poseidonVersion >= 'v1.5.0') {
// don't need it after v1.5.0
unset($json->inboundDetour);
unset($json->stats);
unset($json->api);
array_shift($json->routing->rules);
}
foreach($json->policy->levels as &$level) {
$level->handshake = 2;
$level->uplinkOnly = 2;
$level->downlinkOnly = 2;
$level->connIdle = 60;
}
return $this->success($json);
} catch (\Exception $e) {
return $this->error($e->getMessage(), 500);
}
}
protected function verifyToken(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
return $this->error("token must be set");
}
if ($token !== config('v2board.server_token')) {
return $this->error("invalid token");
}
}
protected function error($msg, int $status = 400) {
return response([
'msg' => $msg,
], $status);
}
protected function success($data) {
$req = request();
// Only for "GET" method
if (!$req->isMethod('GET') || !$data) {
return response([
'msg' => 'ok',
'data' => $data,
]);
}
$etag = sha1(json_encode($data));
if ($etag == $req->header("IF-NONE-MATCH")) {
return response(null, 304);
}
return response([
'msg' => 'ok',
'data' => $data,
])->header('ETAG', $etag);
}
}

View File

@ -8,10 +8,6 @@ use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
@ -41,7 +37,7 @@ class ShadowsocksTidalabController extends Controller
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
array_push($result, [
@ -72,23 +68,11 @@ class ShadowsocksTidalabController extends Controller
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch((float)$u, (float)$d, (int)$item['user_id'], $server, 'shadowsocks')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'shadowsocks');
}
DB::commit();
return response([
'ret' => 1,

View File

@ -41,7 +41,7 @@ class TrojanTidalabController extends Controller
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->trojan_user = [
@ -73,23 +73,11 @@ class TrojanTidalabController extends Controller
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'trojan')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'trojan');
}
DB::commit();
return response([
'ret' => 1,

View File

@ -14,9 +14,12 @@ class CommController extends Controller
return response([
'data' => [
'is_telegram' => (int)config('v2board.telegram_bot_enable', 0),
'telegram_discuss_link' => config('v2board.telegram_discuss_link'),
'stripe_pk' => config('v2board.stripe_pk_live'),
'withdraw_methods' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0)
'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
]
]);
}
@ -27,9 +30,8 @@ class CommController extends Controller
->where('payment', 'StripeCredit')
->first();
if (!$payment) abort(500, 'payment is not found');
$config = json_decode($payment->config, true);
return response([
'data' => $config['stripe_pk_live']
'data' => $payment->config['stripe_pk_live']
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
use App\Models\Coupon;
@ -13,27 +14,12 @@ class CouponController extends Controller
if (empty($request->input('code'))) {
abort(500, __('Coupon cannot be empty'));
}
$coupon = Coupon::where('code', $request->input('code'))->first();
if (!$coupon) {
abort(500, __('Invalid coupon'));
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, __('This coupon is no longer available'));
}
if (time() < $coupon->started_at) {
abort(500, __('This coupon has not yet started'));
}
if (time() > $coupon->ended_at) {
abort(500, __('This coupon has expired'));
}
if ($coupon->limit_plan_ids) {
$limitPlanIds = json_decode($coupon->limit_plan_ids);
if (!in_array($request->input('plan_id'), $limitPlanIds)) {
abort(500, __('The coupon code cannot be used for this subscription'));
}
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->session()->get('id'));
$couponService->check();
return response([
'data' => $coupon
'data' => $couponService->getCoupon()
]);
}
}

View File

@ -29,7 +29,7 @@ class InviteController extends Controller
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('commission_balance', '>', 0)
->where('status', 3)
->whereIn('status', [3, 4])
->select([
'id',
'commission_status',

View File

@ -35,8 +35,6 @@ class KnowledgeController extends Controller
}
$subscribeUrl = "{$subscribeUrl}/api/v1/client/subscribe?token={$user['token']}";
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{appleId}}', $appleId, $knowledge['body']);
$knowledge['body'] = str_replace('{{appleIdPassword}}', $appleIdPassword, $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
$knowledge['body'] = str_replace(

View File

@ -78,26 +78,27 @@ class OrderController extends Controller
abort(500, __('Subscription plan does not exist'));
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('cycle') !== 'reset_price') {
abort(500, __('This subscription has been sold out, please choose another subscription'));
}
if ($plan[$request->input('period')] === NULL) {
abort(500, __('This payment period cannot be purchased, please choose another period'));
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('cycle') !== 'reset_price') {
abort(500, __('This subscription cannot be renewed, please change to another subscription'));
}
if ($plan[$request->input('cycle')] === NULL) {
abort(500, __('This payment cycle cannot be purchased, please choose another cycle'));
}
if ($request->input('cycle') === 'reset_price') {
if ($request->input('period') === 'reset_price') {
if ($user->expired_at <= time() || !$user->plan_id) {
abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('period') !== 'reset_price') {
abort(500, __('This subscription has been sold out, please choose another subscription'));
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
abort(500, __('This subscription cannot be renewed, please change to another subscription'));
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('This subscription has expired, please change to another subscription'));
}
@ -107,9 +108,9 @@ class OrderController extends Controller
$orderService = new OrderService($order);
$order->user_id = $request->session()->get('id');
$order->plan_id = $plan->id;
$order->cycle = $request->input('cycle');
$order->trade_no = Helper::guid();
$order->total_amount = $plan[$request->input('cycle')];
$order->period = $request->input('period');
$order->trade_no = Helper::generateOrderNo();
$order->total_amount = $plan[$request->input('period')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
@ -169,9 +170,8 @@ class OrderController extends Controller
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) abort(500, '');
return response([
'type' => -1,
'data' => true
@ -212,7 +212,8 @@ class OrderController extends Controller
$methods = Payment::select([
'id',
'name',
'payment'
'payment',
'icon'
])
->where('enable', 1)->get();

View File

@ -8,7 +8,7 @@ use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerLog;
use App\Models\User;
@ -36,16 +36,16 @@ class ServerController extends Controller
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$serverLogModel = ServerLog::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC');
->orderBy('log_at', 'DESC');
switch ($type) {
case 0:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')));
break;
case 1:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-1')));
}
$total = $serverLogModel->count();
$res = $serverLogModel->forPage($current, $pageSize)

View File

@ -168,7 +168,7 @@ class TicketController extends Controller
$user = User::find($request->session()->get('id'));
$limit = config('v2board.commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
abort(500, __('The current required minimum withdrawal commission is', ['limit' => $limit]));
abort(500, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit]));
}
DB::beginTransaction();
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');

View File

@ -6,14 +6,16 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Ticket;
use App\Utils\Helper;
use App\Models\Order;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
@ -33,6 +35,7 @@ class UserController extends Controller
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$request->input('old_password'),
$user->password)
) {
@ -40,6 +43,7 @@ class UserController extends Controller
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, __('Save failed'));
}
@ -66,7 +70,8 @@ class UserController extends Controller
'plan_id',
'discount',
'commission_rate',
'telegram_id'
'telegram_id',
'uuid'
])
->first();
if (!$user) {
@ -99,7 +104,6 @@ class UserController extends Controller
{
$user = User::where('id', $request->session()->get('id'))
->select([
'id',
'plan_id',
'token',
'expired_at',
@ -118,12 +122,7 @@ class UserController extends Controller
abort(500, __('Subscription plan does not exist'));
}
}
$subscribeUrl = config('v2board.app_url', env('APP_URL'));
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
$user['subscribe_url'] = "{$subscribeUrl}/api/v1/client/subscribe?token={$user['token']}";
$user['subscribe_url'] = Helper::getSubscribeHost() . "/api/v1/client/subscribe?token={$user['token']}";
$user['reset_day'] = $this->getResetDay($user);
return response([
'data' => $user
@ -190,6 +189,8 @@ class UserController extends Controller
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
// if reset method is not reset
if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
@ -209,4 +210,26 @@ class UserController extends Controller
}
return null;
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('The user does not exist'));
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$url = config('v2board.app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
}

View File

@ -35,6 +35,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CORS::class,
],
'api' => [

View File

@ -15,8 +15,9 @@ class User
*/
public function handle($request, Closure $next)
{
if ($request->input('auth_data')) {
$authData = explode(':', base64_decode($request->input('auth_data')));
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if ($authorization) {
$authData = explode(':', base64_decode($authorization));
if (!isset($authData[1]) || !isset($authData[0])) abort(403, '鉴权失败,请重新登入');
$user = \App\Models\User::where('password', $authData[1])
->where('email', $authData[0])

View File

@ -25,6 +25,10 @@ class ConfigSave extends FormRequest
'commission_withdraw_limit' => 'nullable|numeric',
'commission_withdraw_method' => 'nullable|array',
'withdraw_close_enable' => 'in:0,1',
'commission_distribution_enable' => 'in:0,1',
'commission_distribution_l1' => 'nullable|numeric',
'commission_distribution_l2' => 'nullable|numeric',
'commission_distribution_l3' => 'nullable|numeric',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
@ -42,9 +46,11 @@ class ConfigSave extends FormRequest
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'tos_url' => 'nullable|url',
'currency' => '',
'currency_symbol' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2',
'surplus_enable' => 'in:0,1',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
@ -88,14 +94,11 @@ class ConfigSave extends FormRequest
'frontend_theme' => '',
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_theme_color' => 'in:default,darkblue,black,green',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
'frontend_customer_service_method' => '',
'frontend_customer_service_id' => '',
// tutorial
'apple_id' => 'nullable|email',
'apple_id_password' => '',
// email
'email_template' => '',
'email_host' => '',
@ -109,6 +112,7 @@ class ConfigSave extends FormRequest
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_id' => '',
'telegram_discuss_link' => 'nullable|url',
// app
'windows_version' => '',
'windows_download_url' => '',
@ -126,7 +130,8 @@ class ConfigSave extends FormRequest
'app_url.url' => '站点URL格式不正确必须携带http(s)://',
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://',
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确'
'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://',
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://'
];
}
}

View File

@ -21,7 +21,9 @@ class CouponGenerate extends FormRequest
'started_at' => 'required|integer',
'ended_at' => 'required|integer',
'limit_use' => 'nullable|integer',
'limit_use_with_user' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'limit_period' => 'nullable|array',
'code' => ''
];
}
@ -40,8 +42,10 @@ class CouponGenerate extends FormRequest
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误'
'limit_use.integer' => '最大使用次数格式有误',
'limit_use_with_user.integer' => '限制用户使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误',
'limit_period.array' => '指定周期格式有误'
];
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class CouponSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'type' => 'required|in:1,2',
'value' => 'required|integer',
'started_at' => 'required|integer',
'ended_at' => 'required|integer',
'limit_use' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'code' => ''
];
}
public function messages()
{
return [
'name.required' => '名称不能为空',
'type.required' => '类型不能为空',
'type.in' => '类型格式有误',
'value.required' => '金额或比例不能为空',
'value.integer' => '金额或比例格式有误',
'started_at.required' => '开始时间不能为空',
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误'
];
}
}

View File

@ -17,7 +17,7 @@ class OrderAssign extends FormRequest
'plan_id' => 'required',
'email' => 'required',
'total_amount' => 'required',
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
];
}
@ -27,8 +27,8 @@ class OrderAssign extends FormRequest
'plan_id.required' => '订阅不能为空',
'email.required' => '邮箱不能为空',
'total_amount.required' => '支付金额不能为空',
'cycle.required' => '订阅周期不能为空',
'cycle.in' => '订阅周期格式有误'
'period.required' => '订阅周期不能为空',
'period.in' => '订阅周期格式有误'
];
}
}

View File

@ -14,7 +14,7 @@ class OrderFetch extends FormRequest
public function rules()
{
return [
'filter.*.key' => 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id',
'filter.*.key' => 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id,callback_no',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => ''
];

View File

@ -25,7 +25,8 @@ class PlanSave extends FormRequest
'two_year_price' => 'nullable|integer',
'three_year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer'
'reset_price' => 'nullable|integer',
'reset_traffic_method' => 'nullable|integer|in:0,1,2'
];
}
@ -44,7 +45,9 @@ class PlanSave extends FormRequest
'two_year_price.integer' => '两年付金额格式有误',
'three_year_price.integer' => '三年付金额格式有误',
'onetime_price.integer' => '一次性金额有误',
'reset_price.integer' => '流量重置包金额有误'
'reset_price.integer' => '流量重置包金额有误',
'reset_traffic_method.integer' => '流量重置方式格式有误',
'reset_traffic_method.in' => '流量重置方式格式有误'
];
}
}

View File

@ -26,10 +26,10 @@ class ServerV2raySave extends FormRequest
'rate' => 'required|numeric',
'alter_id' => 'required|integer',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic,grpc',
'networkSettings' => '',
'ruleSettings' => '',
'tlsSettings' => '',
'dnsSettings' => ''
'networkSettings' => 'nullable|array',
'ruleSettings' => 'nullable|array',
'tlsSettings' => 'nullable|array',
'dnsSettings' => 'nullable|array'
];
}
@ -48,7 +48,11 @@ class ServerV2raySave extends FormRequest
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'network.required' => '传输协议不能为空',
'network.in' => '传输协议格式不正确'
'network.in' => '传输协议格式不正确',
'networkSettings.array' => '传输协议配置有误',
'ruleSettings.array' => '规则配置有误',
'tlsSettings.array' => 'tls配置有误',
'dnsSettings.array' => 'dns配置有误'
];
}
}

View File

@ -15,7 +15,7 @@ class OrderSave extends FormRequest
{
return [
'plan_id' => 'required',
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
];
}
@ -23,8 +23,8 @@ class OrderSave extends FormRequest
{
return [
'plan_id.required' => __('Plan ID cannot be empty'),
'cycle.required' => __('Plan cycle cannot be empty'),
'cycle.in' => __('Wrong plan cycle')
'period.required' => __('Plan period cannot be empty'),
'period.in' => __('Wrong plan period')
];
}
}

View File

@ -17,6 +17,7 @@ class AdminRoute
$router->get ('/config/getEmailTemplate', 'Admin\\ConfigController@getEmailTemplate');
$router->get ('/config/getThemeTemplate', 'Admin\\ConfigController@getThemeTemplate');
$router->post('/config/setTelegramWebhook', 'Admin\\ConfigController@setTelegramWebhook');
$router->post('/config/testSendMail', 'Admin\\ConfigController@testSendMail');
// Plan
$router->get ('/plan/fetch', 'Admin\\PlanController@fetch');
$router->post('/plan/save', 'Admin\\PlanController@save');
@ -63,9 +64,10 @@ class AdminRoute
});
// Order
$router->get ('/order/fetch', 'Admin\\OrderController@fetch');
$router->post('/order/repair', 'Admin\\OrderController@repair');
$router->post('/order/update', 'Admin\\OrderController@update');
$router->post('/order/assign', 'Admin\\OrderController@assign');
$router->post('/order/paid', 'Admin\\OrderController@paid');
$router->post('/order/cancel', 'Admin\\OrderController@cancel');
// User
$router->get ('/user/fetch', 'Admin\\UserController@fetch');
$router->post('/user/update', 'Admin\\UserController@update');

View File

@ -20,6 +20,7 @@ class UserRoute
$router->get ('/getSubscribe', 'User\\UserController@getSubscribe');
$router->get ('/getStat', 'User\\UserController@getStat');
$router->post('/transfer', 'User\\UserController@transfer');
$router->post('/getQuickLoginUrl', 'User\\UserController@getQuickLoginUrl');
// Order
$router->post('/order/save', 'User\\OrderController@save');
$router->post('/order/checkout', 'User\\OrderController@checkout');

View File

@ -0,0 +1,52 @@
<?php
namespace App\Jobs;
use App\Models\Order;
use App\Services\OrderService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class OrderHandleJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $order;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($tradeNo)
{
$this->onQueue('order_handle');
$this->order = Order::where('trade_no', $tradeNo)
->lockForUpdate()
->first();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (!$this->order) return;
$orderService = new OrderService($this->order);
switch ($this->order->status) {
// cancel
case 0:
if ($this->order->created_at <= (time() - 1800)) {
$orderService->cancel();
}
break;
case 1:
$orderService->open();
break;
}
}
}

View File

@ -21,10 +21,9 @@ class SendEmailJob implements ShouldQueue
*
* @return void
*/
public function __construct($params)
public function __construct($params, $queue = 'send_email')
{
$this->delay(now()->addSecond(2));
$this->onQueue('send_email');
$this->onQueue($queue);
$this->params = $params;
}
@ -60,11 +59,15 @@ class SendEmailJob implements ShouldQueue
$error = $e->getMessage();
}
MailLog::create([
$log = [
'email' => $params['email'],
'subject' => $params['subject'],
'template_name' => $params['template_name'],
'error' => isset($error) ? $error : NULL
]);
];
MailLog::create($log);
$log['config'] = config('mail');
return $log;
}
}

58
app/Jobs/ServerLogJob.php Normal file
View File

@ -0,0 +1,58 @@
<?php
namespace App\Jobs;
use App\Services\ServerService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerLogJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $u;
protected $d;
protected $userId;
protected $server;
protected $protocol;
public $tries = 3;
public $timeout = 3;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($u, $d, $userId, $server, $protocol)
{
$this->onQueue('server_log');
$this->u = $u;
$this->d = $d;
$this->userId = $userId;
$this->server = $server;
$this->protocol = $protocol;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$serverService = new ServerService();
if (!$serverService->log(
$this->userId,
$this->server->id,
$this->u,
$this->d,
$this->server->rate,
$this->protocol
)) {
throw new \Exception('日志记录失败');
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Jobs;
use App\Models\User;
use App\Services\MailService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class TrafficFetchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $u;
protected $d;
protected $userId;
protected $server;
protected $protocol;
public $tries = 3;
public $timeout = 3;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($u, $d, $userId, $server, $protocol)
{
$this->onQueue('traffic_fetch');
$this->u = $u;
$this->d = $d;
$this->userId = $userId;
$this->server = $server;
$this->protocol = $protocol;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$user = User::lockForUpdate()->find($this->userId);
if (!$user) return;
$user->t = time();
$user->u = $user->u + $this->u;
$user->d = $user->d + $this->d;
if (!$user->save()) throw new \Exception('流量更新失败');
$mailService = new MailService();
$mailService->remindTraffic($user);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CommissionLog extends Model
{
protected $table = 'v2_commission_log';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,10 @@ class Coupon extends Model
protected $table = 'v2_coupon';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'limit_plan_ids' => 'array',
'limit_period' => 'array'
];
}

View File

@ -8,4 +8,8 @@ class InviteCode extends Model
{
protected $table = 'v2_invite_code';
protected $dateFormat = 'U';
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class Knowledge extends Model
protected $table = 'v2_knowledge';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class MailLog extends Model
protected $table = 'v2_mail_log';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class Notice extends Model
protected $table = 'v2_notice';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,9 @@ class Order extends Model
protected $table = 'v2_order';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'surplus_order_ids' => 'array'
];
}

View File

@ -9,4 +9,9 @@ class Payment extends Model
protected $table = 'v2_payment';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'config' => 'array'
];
}

View File

@ -9,4 +9,8 @@ class Plan extends Model
protected $table = 'v2_plan';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -1,12 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Server extends Model
{
protected $table = 'v2_server';
protected $dateFormat = 'U';
protected $guarded = ['id'];
}

View File

@ -8,4 +8,8 @@ class ServerGroup extends Model
{
protected $table = 'v2_server_group';
protected $dateFormat = 'U';
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class ServerLog extends Model
{
protected $table = 'v2_server_log';
protected $dateFormat = 'U';
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,10 @@ class ServerShadowsocks extends Model
protected $table = 'v2_server_shadowsocks';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'tags' => 'array'
];
}

View File

@ -9,4 +9,8 @@ class ServerStat extends Model
protected $table = 'v2_server_stat';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,10 @@ class ServerTrojan extends Model
protected $table = 'v2_server_trojan';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'tags' => 'array'
];
}

22
app/Models/ServerV2ray.php Executable file
View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ServerV2ray extends Model
{
protected $table = 'v2_server_v2ray';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'group_id' => 'array',
'tlsSettings' => 'array',
'networkSettings' => 'array',
'dnsSettings' => 'array',
'ruleSettings' => 'array',
'tags' => 'array'
];
}

View File

@ -9,4 +9,8 @@ class StatOrder extends Model
protected $table = 'v2_stat_order';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class StatServer extends Model
protected $table = 'v2_stat_server';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class Ticket extends Model
protected $table = 'v2_ticket';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class TicketMessage extends Model
protected $table = 'v2_ticket_message';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -9,4 +9,8 @@ class User extends Model
protected $table = 'v2_user';
protected $dateFormat = 'U';
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'timestamp',
'updated_at' => 'timestamp'
];
}

View File

@ -8,6 +8,8 @@ namespace App\Payments;
use \Curl\Curl;
class MGate {
private $config;
public function __construct($config)
{
$this->config = $config;
@ -47,6 +49,7 @@ class MGate {
$str = http_build_query($params) . $this->config['mgate_app_secret'];
$params['sign'] = md5($str);
$curl = new Curl();
$curl->setUserAgent('MGate');
$curl->post($this->config['mgate_url'] . '/v1/gateway/fetch', http_build_query($params));
$result = $curl->response;
if (!$result) {

View File

@ -0,0 +1,43 @@
<?php
namespace App\Providers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
parent::boot();
// Horizon::routeSmsNotificationsTo('15556667777');
// Horizon::routeMailNotificationsTo('example@example.com');
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
// Horizon::night();
}
/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*
* @return void
*/
protected function gate()
{
Gate::define('viewHorizon', function ($user) {
return in_array($user->email, [
//
]);
});
}
}

View File

@ -8,27 +8,22 @@ use Illuminate\Support\Facades\DB;
class CouponService
{
public $order;
public $coupon;
public $planId;
public $userId;
public $period;
public function __construct($code)
{
$this->coupon = Coupon::where('code', $code)->first();
if (!$this->coupon) {
abort(500, '优惠券无效');
}
if ($this->coupon->limit_use <= 0 && $this->coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
}
if (time() < $this->coupon->started_at) {
abort(500, '优惠券还未到可用时间');
}
if (time() > $this->coupon->ended_at) {
abort(500, '优惠券已过期');
}
}
public function use(Order $order)
public function use(Order $order):bool
{
$this->setPlanId($order->plan_id);
$this->setUserId($order->user_id);
$this->setPeriod($order->period);
$this->check();
switch ($this->coupon->type) {
case 1:
$order->discount_amount = $this->coupon->value;
@ -37,18 +32,15 @@ class CouponService
$order->discount_amount = $order->total_amount * ($this->coupon->value / 100);
break;
}
if ($order->discount_amount > $order->total_amount) {
$order->discount_amount = $order->total_amount;
}
if ($this->coupon->limit_use !== NULL) {
$this->coupon->limit_use = $this->coupon->limit_use - 1;
if (!$this->coupon->save()) {
return false;
}
}
if ($this->coupon->limit_plan_ids) {
$limitPlanIds = json_decode($this->coupon->limit_plan_ids);
if (!in_array($order->plan_id, $limitPlanIds)) {
return false;
}
}
return true;
}
@ -56,4 +48,67 @@ class CouponService
{
return $this->coupon->id;
}
public function getCoupon()
{
return $this->coupon;
}
public function setPlanId($planId)
{
$this->planId = $planId;
}
public function setUserId($userId)
{
$this->userId = $userId;
}
public function setPeriod($period)
{
$this->period = $period;
}
public function checkLimitUseWithUser():bool
{
$usedCount = Order::where('coupon_id', $this->coupon->id)
->where('user_id', $this->userId)
->whereNotIn('status', [0, 2])
->count();
if ($usedCount >= $this->coupon->limit_use_with_user) return false;
return true;
}
public function check()
{
if (!$this->coupon) {
abort(500, __('Invalid coupon'));
}
if ($this->coupon->limit_use <= 0 && $this->coupon->limit_use !== NULL) {
abort(500, __('This coupon is no longer available'));
}
if (time() < $this->coupon->started_at) {
abort(500, __('This coupon has not yet started'));
}
if (time() > $this->coupon->ended_at) {
abort(500, __('This coupon has expired'));
}
if ($this->coupon->limit_plan_ids && $this->planId) {
if (!in_array($this->planId, $this->coupon->limit_plan_ids)) {
abort(500, __('The coupon code cannot be used for this subscription'));
}
}
if ($this->coupon->limit_period && $this->period) {
if (!in_array($this->period, $this->coupon->limit_period)) {
abort(500, __('The coupon code cannot be used for this period'));
}
}
if ($this->coupon->limit_use_with_user !== NULL && $this->userId) {
if (!$this->checkLimitUseWithUser()) {
abort(500, __('The coupon can only be used :limit_use_with_user per person', [
'limit_use_with_user' => $this->coupon->limit_use_with_user
]));
}
}
}
}

View File

@ -12,13 +12,15 @@ class MailService
public function remindTraffic (User $user)
{
if (!$user->remind_traffic) return;
if (!$this->remindTrafficIsWarnValue(($user->u + $user->d), $user->transfer_enable)) return;
if (!$this->remindTrafficIsWarnValue($user->u, $user->d, $user->transfer_enable)) return;
$flag = CacheKey::get('LAST_SEND_EMAIL_REMIND_TRAFFIC', $user->id);
if (Cache::get($flag)) return;
if (!Cache::put($flag, 1, 24 * 3600)) return;
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的流量使用已达到80%',
'subject' => __('The traffic usage in :app_name has reached 80%', [
'app_name' => config('v2board.app_name', 'V2board')
]),
'template_name' => 'remindTraffic',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
@ -27,8 +29,25 @@ class MailService
]);
}
private function remindTrafficIsWarnValue($ud, $transfer_enable)
public function remindExpire(User $user)
{
if (!($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time())) return;
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => __('The service in :app_name is about to expire', [
'app_name' => config('v2board.app_name', 'V2board')
]),
'template_name' => 'remindExpire',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
}
private function remindTrafficIsWarnValue($u, $d, $transfer_enable)
{
$ud = $u + $d;
if (!$ud) return false;
if (!$transfer_enable) return false;
$percentage = ($ud / $transfer_enable) * 100;

View File

@ -2,6 +2,7 @@
namespace App\Services;
use App\Jobs\OrderHandleJob;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
@ -37,7 +38,7 @@ class OrderService
DB::beginTransaction();
if ($order->surplus_order_ids) {
try {
Order::whereIn('id', json_decode($order->surplus_order_ids))->update([
Order::whereIn('id', $order->surplus_order_ids)->update([
'status' => 4
]);
} catch (\Exception $e) {
@ -45,7 +46,7 @@ class OrderService
abort(500, '开通失败');
}
}
switch ((string)$order->cycle) {
switch ((string)$order->period) {
case 'onetime_price':
$this->buyByOneTime($plan);
break;
@ -53,7 +54,7 @@ class OrderService
$this->buyByResetTraffic();
break;
default:
$this->buyByCycle($order, $plan);
$this->buyByPeriod($order, $plan);
}
switch ((int)$order->type) {
@ -81,30 +82,11 @@ class OrderService
DB::commit();
}
public function cancel():bool
{
$order = $this->order;
DB::beginTransaction();
$order->status = 2;
if (!$order->save()) {
DB::rollBack();
return false;
}
if ($order->balance_amount) {
$userService = new UserService();
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
DB::rollBack();
return false;
}
}
DB::commit();
return true;
}
public function setOrderType(User $user)
{
$order = $this->order;
if ($order->cycle === 'reset_price') {
if ($order->period === 'reset_price') {
$order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && ($user->expired_at > time() || $user->expired_at === NULL)) {
if (!(int)config('v2board.plan_change_enable', 1)) abort(500, '目前不允许更改订阅,请联系客服或提交工单操作');
@ -174,7 +156,7 @@ class OrderService
if ($user->expired_at === NULL) {
$this->getSurplusValueByOneTime($user, $order);
} else {
$this->getSurplusValueByCycle($user, $order);
$this->getSurplusValueByPeriod($user, $order);
}
}
@ -188,23 +170,23 @@ class OrderService
}
$notUsedTraffic = $plan->transfer_enable - (($user->u + $user->d) / 1073741824);
$result = $trafficUnitPrice * $notUsedTraffic;
$orderModel = Order::where('user_id', $user->id)->where('cycle', '!=', 'reset_price')->where('status', 3);
$orderModel = Order::where('user_id', $user->id)->where('period', '!=', 'reset_price')->where('status', 3);
$order->surplus_amount = $result > 0 ? $result : 0;
$order->surplus_order_ids = json_encode(array_column($orderModel->get()->toArray(), 'id'));
$order->surplus_order_ids = array_column($orderModel->get()->toArray(), 'id');
}
private function orderIsUsed(Order $order):bool
{
$month = self::STR_TO_TIME[$order->cycle];
$orderExpireDay = strtotime('+' . $month . ' month', $order->created_at->timestamp);
$month = self::STR_TO_TIME[$order->period];
$orderExpireDay = strtotime('+' . $month . ' month', $order->created_at);
if ($orderExpireDay < time()) return true;
return false;
}
private function getSurplusValueByCycle(User $user, Order $order)
private function getSurplusValueByPeriod(User $user, Order $order)
{
$orderModel = Order::where('user_id', $user->id)
->where('cycle', '!=', 'reset_price')
->where('period', '!=', 'reset_price')
->where('status', 3);
$orders = $orderModel->get();
$orderSurplusMonth = 0;
@ -212,9 +194,9 @@ class OrderService
$userSurplusMonth = ($user->expired_at - time()) / 2678400;
foreach ($orders as $k => $item) {
// 兼容历史余留问题
if ($item->cycle === 'onetime_price') continue;
if ($item->period === 'onetime_price') continue;
if ($this->orderIsUsed($item)) continue;
$orderSurplusMonth = $orderSurplusMonth + self::STR_TO_TIME[$item->cycle];
$orderSurplusMonth = $orderSurplusMonth + self::STR_TO_TIME[$item->period];
$orderSurplusAmount = $orderSurplusAmount + ($item['total_amount'] + $item['balance_amount'] + $item['surplus_amount'] - $item['refund_amount']);
}
if (!$orderSurplusMonth || !$orderSurplusAmount) return;
@ -229,20 +211,40 @@ class OrderService
return;
}
$order->surplus_amount = $orderSurplusAmount > 0 ? $orderSurplusAmount : 0;
$order->surplus_order_ids = json_encode(array_column($orders->toArray(), 'id'));
$order->surplus_order_ids = array_column($orders->toArray(), 'id');
}
public function success(string $callbackNo)
public function paid(string $callbackNo)
{
$order = $this->order;
if ($order->status !== 0) {
return true;
}
if ($order->status !== 0) return true;
$order->status = 1;
$order->paid_at = time();
$order->callback_no = $callbackNo;
return $order->save();
if (!$order->save()) return false;
OrderHandleJob::dispatch($order->trade_no);
return true;
}
public function cancel():bool
{
$order = $this->order;
DB::beginTransaction();
$order->status = 2;
if (!$order->save()) {
DB::rollBack();
return false;
}
if ($order->balance_amount) {
$userService = new UserService();
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
DB::rollBack();
return false;
}
}
DB::commit();
return true;
}
private function buyByResetTraffic()
{
@ -250,7 +252,7 @@ class OrderService
$this->user->d = 0;
}
private function buyByCycle(Order $order, Plan $plan)
private function buyByPeriod(Order $order, Plan $plan)
{
// change plan process
if ((int)$order->type === 3) {
@ -263,7 +265,7 @@ class OrderService
if ($order->type === 1) $this->buyByResetTraffic();
$this->user->plan_id = $plan->id;
$this->user->group_id = $plan->group_id;
$this->user->expired_at = $this->getTime($order->cycle, $this->user->expired_at);
$this->user->expired_at = $this->getTime($order->period, $this->user->expired_at);
}
private function buyByOneTime(Plan $plan)

View File

@ -22,7 +22,7 @@ class PaymentService
if ($uuid) $payment = Payment::where('uuid', $uuid)->first()->toArray();
$this->config = [];
if (isset($payment)) {
$this->config = json_decode($payment['config'], true);
$this->config = $payment['config'];
$this->config['enable'] = $payment['enable'];
$this->config['id'] = $payment['id'];
$this->config['uuid'] = $payment['uuid'];

View File

@ -5,7 +5,7 @@ namespace App\Services;
use App\Models\ServerLog;
use App\Models\ServerShadowsocks;
use App\Models\User;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerTrojan;
use App\Utils\CacheKey;
use Illuminate\Support\Facades\Cache;
@ -18,14 +18,14 @@ class ServerService
public function getV2ray(User $user, $all = false):array
{
$servers = [];
$model = Server::orderBy('sort', 'ASC');
$model = ServerV2ray::orderBy('sort', 'ASC');
if (!$all) {
$model->where('show', 1);
}
$v2ray = $model->get();
for ($i = 0; $i < count($v2ray); $i++) {
$v2ray[$i]['type'] = 'v2ray';
$groupId = json_decode($v2ray[$i]['group_id']);
$groupId = $v2ray[$i]['group_id'];
if (in_array($user->group_id, $groupId)) {
if ($v2ray[$i]['parent_id']) {
$v2ray[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $v2ray[$i]['parent_id']));
@ -50,7 +50,7 @@ class ServerService
$trojan = $model->get();
for ($i = 0; $i < count($trojan); $i++) {
$trojan[$i]['type'] = 'trojan';
$groupId = json_decode($trojan[$i]['group_id']);
$groupId = $trojan[$i]['group_id'];
if (in_array($user->group_id, $groupId)) {
if ($trojan[$i]['parent_id']) {
$trojan[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $trojan[$i]['parent_id']));
@ -73,7 +73,7 @@ class ServerService
$shadowsocks = $model->get();
for ($i = 0; $i < count($shadowsocks); $i++) {
$shadowsocks[$i]['type'] = 'shadowsocks';
$groupId = json_decode($shadowsocks[$i]['group_id']);
$groupId = $shadowsocks[$i]['group_id'];
if (in_array($user->group_id, $groupId)) {
if ($shadowsocks[$i]['parent_id']) {
$shadowsocks[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $shadowsocks[$i]['parent_id']));
@ -123,7 +123,7 @@ class ServerService
public function getV2RayConfig(int $nodeId, int $localPort)
{
$server = Server::find($nodeId);
$server = ServerV2ray::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
@ -156,10 +156,10 @@ class ServerService
return $json;
}
private function setDns(Server $server, object $json)
private function setDns(ServerV2ray $server, object $json)
{
if ($server->dnsSettings) {
$dns = json_decode($server->dnsSettings);
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
@ -169,41 +169,41 @@ class ServerService
}
}
private function setNetwork(Server $server, object $json)
private function setNetwork(ServerV2ray $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbound->streamSettings->tcpSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbound->streamSettings->kcpSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbound->streamSettings->wsSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbound->streamSettings->httpSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbound->streamSettings->dsSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbound->streamSettings->quicSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbound->streamSettings->grpcSettings = json_decode($server->networkSettings);
$json->inbound->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(Server $server, object $json)
private function setRule(ServerV2ray $server, object $json)
{
$domainRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_protocol')));
if ($server->ruleSettings) {
$ruleSettings = json_decode($server->ruleSettings);
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->domain)) {
$ruleSettings->domain = array_filter($ruleSettings->domain);
@ -238,10 +238,10 @@ class ServerService
}
}
private function setTls(Server $server, object $json)
private function setTls(ServerV2ray $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = json_decode($server->tlsSettings);
$tlsSettings = $server->tlsSettings;
$json->inbound->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
@ -260,20 +260,23 @@ class ServerService
public function log(int $userId, int $serverId, int $u, int $d, float $rate, string $method)
{
if (($u + $d) <= 10240) return;
$timestamp = strtotime(date('Y-m-d H:0'));
if (($u + $d) < 10240) return true;
$timestamp = strtotime(date('Y-m-d'));
$serverLog = ServerLog::where('log_at', '>=', $timestamp)
->where('log_at', '<', $timestamp + 3600)
->where('server_id', $serverId)
->where('user_id', $userId)
->where('rate', $rate)
->where('method', $method)
->lockForUpdate()
->first();
if ($serverLog) {
$serverLog->u = $serverLog->u + $u;
$serverLog->d = $serverLog->d + $d;
$serverLog->save();
try {
$serverLog->increment('u', $u);
$serverLog->increment('d', $d);
return true;
} catch (\Exception $e) {
return false;
}
} else {
$serverLog = new ServerLog();
$serverLog->user_id = $userId;
@ -283,7 +286,7 @@ class ServerService
$serverLog->rate = $rate;
$serverLog->log_at = $timestamp;
$serverLog->method = $method;
$serverLog->save();
return $serverLog->save();
}
}
@ -292,32 +295,15 @@ class ServerService
$server = ServerShadowsocks::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
$server[$i]['type'] = 'shadowsocks';
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
}
return $server->toArray();
}
public function getV2rayServers()
{
$server = Server::orderBy('sort', 'ASC')->get();
$server = ServerV2ray::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
$server[$i]['type'] = 'v2ray';
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
if (!empty($server[$i]['dnsSettings'])) {
$server[$i]['dnsSettings'] = json_decode($server[$i]['dnsSettings']);
}
if (!empty($server[$i]['tlsSettings'])) {
$server[$i]['tlsSettings'] = json_decode($server[$i]['tlsSettings']);
}
if (!empty($server[$i]['ruleSettings'])) {
$server[$i]['ruleSettings'] = json_decode($server[$i]['ruleSettings']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
}
return $server->toArray();
}
@ -327,10 +313,6 @@ class ServerService
$server = ServerTrojan::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
$server[$i]['type'] = 'trojan';
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
}
return $server->toArray();
}

View File

@ -4,6 +4,7 @@ namespace App\Services;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use \Curl\Curl;
use Illuminate\Mail\Markdown;
class TelegramService {
protected $api;
@ -15,6 +16,9 @@ class TelegramService {
public function sendMessage(int $chatId, string $text, string $parseMode = '')
{
if ($parseMode === 'markdown') {
$text = str_replace('_', '\_', $text);
}
$this->request('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
@ -40,6 +44,7 @@ class TelegramService {
$curl->get($this->api . $method . '?' . http_build_query($params));
$response = $curl->response;
$curl->close();
if (!isset($response->ok)) abort(500, '请求失败');
if (!$response->ok) {
abort(500, '来自TG的错误' . $response->description);
}

View File

@ -12,6 +12,7 @@ use Illuminate\Support\Facades\DB;
class TicketService {
public function replyByAdmin($ticketId, $message, $userId):void
{
if ($message)
$ticket = Ticket::where('id', $ticketId)
->first();
if (!$ticket) {

View File

@ -2,9 +2,11 @@
namespace App\Services;
use App\Jobs\ServerLogJob;
use App\Jobs\TrafficFetchJob;
use App\Models\InviteCode;
use App\Models\Order;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Support\Facades\DB;
@ -80,33 +82,9 @@ class UserService
return true;
}
public function trafficFetch(int $u, int $d, int $userId, object $server, string $protocol):bool
public function trafficFetch(int $u, int $d, int $userId, object $server, string $protocol)
{
$user = User::lockForUpdate()
->find($userId);
if (!$user) {
return true;
}
$user->t = time();
$user->u = $user->u + $u;
$user->d = $user->d + $d;
if (!$user->save()) {
return false;
}
$mailService = new MailService();
$serverService = new ServerService();
try {
$mailService->remindTraffic($user);
$serverService->log(
$userId,
$server->id,
$u,
$d,
$server->rate,
$protocol
);
} catch (\Exception $e) {
}
return true;
TrafficFetchJob::dispatch($u, $d, $userId, $server, $protocol);
ServerLogJob::dispatch($u, $d, $userId, $server, $protocol);
}
}

View File

@ -17,7 +17,7 @@ class CacheKey
'SERVER_SHADOWSOCKS_LAST_CHECK_AT' => 'ss节点最后检查时间',
'SERVER_SHADOWSOCKS_LAST_PUSH_AT' => 'ss节点最后推送时间',
'TEMP_TOKEN' => '临时令牌',
'LAST_SEND_EMAIL_REMIND_TRAFFIC'
'LAST_SEND_EMAIL_REMIND_TRAFFIC' => '最后发送流量邮件提醒'
];
public static function get(string $key, $uniqueValue)

View File

@ -2,7 +2,7 @@
namespace App\Utils;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\User;
@ -23,6 +23,12 @@ class Helper
return md5(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)) . '-' . time());
}
public static function generateOrderNo(): string
{
$randomChar = rand(10000, 99999);
return date('YmdHms') . $randomChar;
}
public static function exchange($from, $to)
{
$result = file_get_contents('https://api.exchangerate.host/latest?symbols=' . $to . '&base=' . $from);
@ -58,11 +64,12 @@ class Helper
return $str;
}
public static function multiPasswordVerify($algo, $password, $hash)
public static function multiPasswordVerify($algo, $salt, $password, $hash)
{
switch($algo) {
case 'md5': return md5($password) === $hash;
case 'sha256': return hash('sha256', $password) === $hash;
case 'md5salt': return md5($password . $salt) === $hash;
default: return password_verify($password, $hash);
}
}
@ -95,4 +102,14 @@ class Helper
return round($byte, 2) . ' B';
}
}
public static function getSubscribeHost()
{
$subscribeUrl = config('v2board.app_url');
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls && $subscribeUrls[0]) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
return $subscribeUrl;
}
}

View File

@ -1,31 +1,37 @@
{
"name": "v2board/v2board",
"type": "project",
"description": "v2board is a v2ray manage.",
"description": "v2board is a proxy protocol manage.",
"keywords": [
"v2board",
"v2ray",
"shadowsocks",
"trojan",
"laravel"
],
"license": "MIT",
"require": {
"php": "^7.2",
"fideloper/proxy": "^4.0",
"php": "^7.2.5|^8.0",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"google/recaptcha": "^1.2",
"laravel/framework": "^6.0",
"laravel/tinker": "^1.0",
"lokielse/omnipay-alipay": "3.0.6",
"guzzlehttp/guzzle": "^6.3.1|^7.0.1",
"laravel/framework": "^7.29",
"laravel/horizon": "^4.3.5",
"laravel/tinker": "^2.5",
"linfo/linfo": "^4.0",
"lokielse/omnipay-alipay": "3.1.2",
"lokielse/omnipay-wechatpay": "^3.0",
"php-curl-class/php-curl-class": "^8.6",
"stripe/stripe-php": "^7.36.1",
"symfony/yaml": "^4.3"
},
"require-dev": {
"facade/ignition": "^1.4",
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^3.0",
"phpunit/phpunit": "^8.0"
"facade/ignition": "^2.0",
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^4.3",
"phpunit/phpunit": "^8.5.8|^9.3.3"
},
"config": {
"optimize-autoloader": true,
@ -65,5 +71,11 @@
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
},
"repositories": {
"packagist": {
"type": "composer",
"url": "https://packagist.org"
}
}
}

View File

@ -173,6 +173,7 @@ return [
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\HorizonServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
@ -236,5 +237,5 @@ return [
| The only modification by laravel config
|
*/
'version' => '1.5.2.1627559775390'
'version' => '1.5.4.1640966110216'
];

34
config/cors.php Normal file
View File

@ -0,0 +1,34 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];

192
config/horizon.php Normal file
View File

@ -0,0 +1,192 @@
<?php
use Illuminate\Support\Str;
use Linfo\Linfo;
$lInfo = new Linfo();
$parser = $lInfo->getParser();
return [
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain.
|
*/
'domain' => null,
/*
|--------------------------------------------------------------------------
| Horizon Path
|--------------------------------------------------------------------------
|
| This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => 'monitor',
/*
|--------------------------------------------------------------------------
| Horizon Redis Connection
|--------------------------------------------------------------------------
|
| This is the name of the Redis connection where Horizon will store the
| meta information required for it to function. It includes the list
| of supervisors, failed jobs, job metrics, and other information.
|
*/
'use' => 'default',
/*
|--------------------------------------------------------------------------
| Horizon Redis Prefix
|--------------------------------------------------------------------------
|
| This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix when you are running multiple installations
| of Horizon on the same server so that they don't have problems.
|
*/
'prefix' => env(
'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
),
/*
|--------------------------------------------------------------------------
| Horizon Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => ['web', 'admin'],
/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired.
|
*/
'waits' => [
'redis:default' => 60,
],
/*
|--------------------------------------------------------------------------
| Job Trimming Times
|--------------------------------------------------------------------------
|
| Here you can configure for how long (in minutes) you desire Horizon to
| persist the recent and failed jobs. Typically, recent jobs are kept
| for one hour while all failed jobs are stored for an entire week.
|
*/
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Metrics
|--------------------------------------------------------------------------
|
| Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
/*
|--------------------------------------------------------------------------
| Fast Termination
|--------------------------------------------------------------------------
|
| When this option is enabled, Horizon's "terminate" command will not
| wait on all of the workers to terminate unless the --wait option
| is provided. Fast termination can shorten deployment delay by
| allowing a new instance of Horizon to start while the last
| instance will continue to terminate each of its workers.
|
*/
'fast_termination' => false,
/*
|--------------------------------------------------------------------------
| Memory Limit (MB)
|--------------------------------------------------------------------------
|
| This value describes the maximum amount of memory the Horizon worker
| may consume before it is terminated and restarted. You should set
| this value according to the resources available to your server.
|
*/
'memory_limit' => 32,
/*
|--------------------------------------------------------------------------
| Queue Worker Configuration
|--------------------------------------------------------------------------
|
| Here you may define the queue worker settings used by your application
| in all environments. These supervisors and settings handle all your
| queued jobs and will be provisioned by Horizon during deployment.
|
*/
'environments' => [
'local' => [
'V2board' => [
'connection' => 'redis',
'queue' => [
'traffic_fetch',
'server_log',
'send_email',
'send_email_mass',
'send_telegram',
'stat_server',
'order_handle',
],
'balance' => 'auto',
'minProcesses' => 1,
'maxProcesses' => (int)ceil($parser->getRam()['total'] / 1024 / 1024 / 1024 * 6),
'tries' => 1,
'nice' => 0,
],
],
],
];

Some files were not shown because too many files have changed in this diff Show More