856 Commits
1.0.1 ... 1.3.2

Author SHA1 Message Date
c1f0521955 Merge pull request #300 from v2board/dev
1.3.2
2020-08-30 00:55:40 +08:00
d9d1947625 update: readme 2020-08-29 21:42:31 +08:00
366cf483ed frontend: email config gui 2020-08-24 13:50:34 +08:00
8c65a7d20e mail: smtp config gui 2020-08-22 14:28:47 +08:00
ce8652fe20 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-08-20 14:31:35 +08:00
03a9e16bb3 order: fix new buy order reset traffic issue 2020-08-20 14:31:16 +08:00
adb201ec86 Merge pull request #294 from betaxab/p1
Quantumult X data usage & expires notification
2020-08-19 22:55:25 +08:00
727c4c8f9f opt: mail config 2020-08-19 15:41:31 +08:00
7c54939970 opt: mail & reset traffic 2020-08-19 12:52:49 +08:00
f3e9d43c44 feature: smtp gui config 2020-08-18 23:27:31 +08:00
c77199ce68 update: frontend 2020-08-18 16:04:18 +08:00
b0afc290e3 quantumultx: add data plan & exipres notification support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-08-16 01:15:59 +08:00
a8a6c6382e update 2020-08-13 21:36:59 +08:00
bd35c782f0 Merge pull request #295 from U-v-U/patch-1
add: added "sni" field for Trojan Link
2020-08-13 20:48:45 +08:00
QxQ
8f9e708e7b add: added "sni" field for Trojan Link 2020-08-13 19:30:07 +08:00
c94ecf1acd feature: update app 2020-08-13 14:27:12 +08:00
654e46a51e feature: telegram unbind & fix bind 2020-08-12 13:33:00 +08:00
8b32002cbd frontend: opt 2020-08-10 21:51:43 +08:00
fa51565928 frontend: opt 2020-08-10 21:49:36 +08:00
95d71ae77f feature: reset day 2020-08-10 21:17:01 +08:00
4f60cc5311 opt: reset server log schedule quarterly 2020-08-10 19:46:06 +08:00
58aacf562c feature: add telegram reply ticket 2020-08-03 16:28:30 +08:00
6a79ba5744 feature: add telegram reply ticket 2020-08-03 16:27:37 +08:00
4a9367158b fix: ticket service 2020-08-03 16:17:03 +08:00
f626acc0aa opt: ticket service 2020-08-03 16:10:56 +08:00
36bc93e1f8 fix: ticket telegram notify 2020-08-02 15:07:28 +08:00
04c06afcff fix: telegram service 2020-08-02 15:05:27 +08:00
c239a83ab3 frontend: plan tag add price 2020-07-31 15:33:56 +08:00
6e7fb4284a telegram: order notify 2020-07-31 15:22:27 +08:00
eb53067e67 vmess: sniffing opt 2020-07-31 15:11:35 +08:00
c9357b602a vmess: sniffing opt 2020-07-31 15:03:12 +08:00
855e3c1c26 Merge pull request #290 from betaxab/p1
Fixes Quantumult X tls-verification
2020-07-29 20:16:30 +08:00
f506d3fe96 quantumultx: fixes tls-verification
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-07-29 20:15:54 +08:00
17d873c8ba Merge pull request #288 from betaxab/patch-1
PHP 7.4 implode compatibility fix
2020-07-28 16:34:52 +08:00
f274cb0d4b PHP 7.4 implode compatibility fix
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-07-28 16:11:27 +08:00
0486f4e6ac frontend: more update 2020-07-26 17:27:42 +08:00
9a68ff6c61 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-07-26 17:23:32 +08:00
1cf0ccb865 feature: add vmess global rules 2020-07-26 17:17:35 +08:00
5b1aee7a79 Merge pull request #285 from betaxab/patch-1
修复 Shadowrocket 空格问题
2020-07-25 16:14:43 +08:00
1b4d03044d quantumultx: fix wss 2020-07-25 15:34:25 +08:00
fb732a8307 frontend: fix user cancel button 2020-07-25 14:54:33 +08:00
3cf39ff045 frontend: progress opt 2020-07-25 02:17:03 +08:00
f80b2cd439 order: fix mgate payment 2020-07-25 00:33:34 +08:00
a75928a91b opcache: fix opcache issue 2020-07-25 00:28:04 +08:00
43193386bb opcache: fix opcache issue 2020-07-25 00:19:13 +08:00
f4b6f0aefe vmess: fix rulesttings is null object 2020-07-24 18:08:36 +08:00
4852e6e79d mgate: fix error report 2020-07-24 02:17:30 +08:00
d8ee3c6c51 shadowrocket: remark space issue fixes, add shadowrocket STATUS feature
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-07-23 12:40:26 +08:00
e3aa467a74 vmess: fix possible config issue 2020-07-22 23:45:00 +08:00
6bbe0e99fa Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-07-22 17:01:09 +08:00
7e3da7b970 trojan: fix child node online count 2020-07-22 17:00:46 +08:00
e096fd064d Merge pull request #278 from DesperadoJ/patch-1
Enable UDP in Clash
2020-07-21 16:19:45 +08:00
9ea482582e Merge pull request #283 from phlinhng/shadowrocket-url
Shadowrocket specified url generator
2020-07-21 12:11:49 +08:00
84d852f396 Shadowrocket specified url generator
shadowrocket ClientController

enable tfo on vmess for shadowrocket

enable tfo on trojan for shadowrocket

fix typo
2020-07-21 00:17:25 +08:00
e1ab2c9f6e frontend: update payment 2020-07-20 19:58:27 +08:00
1f9ebe1ff7 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-07-20 19:58:03 +08:00
cff5d205bc feature: added epay & mGate payment 2020-07-20 19:56:39 +08:00
f8ea476bcc Merge pull request #281 from betaxab/patch-1
clash: add tls servername support
2020-07-18 16:23:00 +08:00
d94b361f2f clash: add tls servername support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-07-18 00:55:24 +08:00
e500fb77dc fix: payment validate 2020-07-17 23:25:46 +08:00
dc98303bb4 feature: add new payment gateway 2020-07-17 21:20:43 +08:00
59761baa4d feature: add custom admin path 2020-07-17 20:01:28 +08:00
b974d1d227 sql: fix historical issues 2020-07-17 19:44:16 +08:00
bb9b1c4a57 optimization: validated 2020-07-15 15:57:12 +08:00
e2a07cc4d1 order: expire user is new order 2020-07-13 17:09:13 +08:00
85fd3d000c Enable UDP in Clash 2020-07-12 11:19:38 +08:00
f204dd2d72 coupon: fix undefined index 2020-07-10 13:21:22 +08:00
137b018aad order: fix reset price buy 2020-07-07 14:11:15 +08:00
72d2b79a9f stripe: add jpy 2020-07-06 19:45:41 +08:00
fcc6332dcd frontend: fix datepicker 2020-07-06 19:40:58 +08:00
e0927de030 Merge pull request #273 from v2board/dev
1.3.1
2020-07-05 19:25:40 +08:00
b706b8b3a1 telegram: fix api 2020-07-05 19:24:25 +08:00
f3dee5c230 Merge pull request #271 from v2board/dev
1.3.1
2020-07-05 14:46:47 +08:00
b984b8dd98 frontend: fix tag 2020-07-05 14:45:44 +08:00
d2589d340d frontend: fix transfer convert 2020-07-05 14:34:03 +08:00
b5376c9c1e Merge pull request #270 from v2board/dev
1.3.1
2020-07-05 14:19:50 +08:00
7c69031db8 update clash default config 2020-07-05 14:17:59 +08:00
43f1dbaedb Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-07-05 14:16:42 +08:00
b083c4fe78 update version 2020-07-05 14:15:52 +08:00
b2c53804d7 Merge pull request #264 from DesperadoJ/dev
Update subscribe URL for Surge & Surfboard
2020-07-05 14:08:30 +08:00
92e6947525 Merge pull request #269 from betaxab/patch-1
surge: Disable network framework
2020-07-05 14:06:50 +08:00
91bf999162 surge: Disable network framework, because it will cause the Surge to stop running
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-07-05 14:03:25 +08:00
4f8ba2b59d support new clash config 2020-07-05 13:18:43 +08:00
001f0ced41 update client 2020-07-04 17:40:23 +08:00
58ad896e45 update client 2020-07-04 17:09:57 +08:00
6e789037d1 fix trojan user online 2020-07-03 15:24:10 +08:00
365775970c update 2020-07-02 21:45:32 +08:00
971637ffd6 update tls path 2020-07-02 21:41:14 +08:00
a905a5ad27 fix trojan server list sort 2020-07-02 01:31:44 +08:00
565072e333 support trojan parent status 2020-07-01 23:08:26 +08:00
dc42e82dc0 update trojan 2020-07-01 18:50:31 +08:00
4c457d183f update trojan 2020-07-01 18:49:51 +08:00
2d67446ce3 update trojan 2020-07-01 18:41:12 +08:00
4c73d55342 update trojan 2020-07-01 18:40:47 +08:00
ee762fe69a update trojan 2020-07-01 18:32:41 +08:00
2782bd1a2c update trojan 2020-07-01 18:31:44 +08:00
2a237abade update trojan 2020-07-01 15:38:23 +08:00
75588d46f1 update trojan 2020-07-01 15:23:39 +08:00
37449b6640 update 2020-07-01 14:08:51 +08:00
0248a562b3 fix 2020-06-27 20:08:16 +08:00
13cfb11a85 optimization 2020-06-26 16:23:11 +08:00
015798accd update plan fetch 2020-06-24 14:26:43 +08:00
f10fc4c13e update version clean cache 2020-06-24 13:56:33 +08:00
b5a9b3e68c more optimization && add plan user count 2020-06-24 13:47:25 +08:00
ffd13fbc64 support clash for android oneclick subscribe 2020-06-21 23:56:09 +08:00
8bff334758 optimize frontend 2020-06-21 13:58:43 +08:00
a63a154ee2 recovery quantumult, next month delete. 2020-06-20 22:59:05 +08:00
976dfd2d98 recovery quantumult, next month delete. 2020-06-20 22:50:43 +08:00
4f277097f6 add custom coupon code 2020-06-20 19:18:57 +08:00
cfd72ac515 update telegram commands 2020-06-20 18:08:26 +08:00
6324645f08 add custom coupon code 2020-06-20 00:50:21 +08:00
80e6fb7186 update server group 2020-06-19 01:25:42 +08:00
13f17d41f0 update trojan server 2020-06-18 20:25:53 +08:00
a45f8e121d update set server copy show default 2020-06-18 17:39:29 +08:00
b6f2a034ec bot add getLatestUrl 2020-06-18 16:26:50 +08:00
d3d18d2390 add server description 2020-06-18 02:25:05 +08:00
65a4abf51d update 2020-06-15 15:38:04 +08:00
0c7d27e331 Update subscribe URL for Surge & Surfboard 2020-06-14 21:48:07 +08:00
ac13d1a8e1 fix subscribe 2020-06-14 12:33:47 +08:00
0f43aee0e3 fix trojan 2020-06-14 02:29:17 +08:00
bbb42c0d46 coupon plan limit 2020-06-13 23:27:43 +08:00
3cdfc69b5d fix trojan server online 2020-06-13 19:02:58 +08:00
bf915214dd support trojan surge 2020-06-13 18:33:42 +08:00
2fc77a38f2 fix trojan shadowrocket remarks 2020-06-12 17:16:21 +08:00
e733a53e85 fix trojan shadowrocket remarks 2020-06-12 17:12:17 +08:00
ff3451d0e0 update trojan server 2020-06-12 17:05:36 +08:00
d418443404 update 2020-06-12 16:59:18 +08:00
172af72761 update trojan server 2020-06-12 14:37:17 +08:00
2414ad5d96 update trojan server 2020-06-12 14:22:00 +08:00
796de25e2d update trojan server 2020-06-12 02:06:56 +08:00
9874ef2f72 update trojan server 2020-06-12 01:31:00 +08:00
da98dcad9c update trojan server 2020-06-12 00:40:37 +08:00
0e4d5c9e99 update trojan server 2020-06-12 00:35:35 +08:00
066fd96fb5 update trojan server 2020-06-12 00:18:35 +08:00
f24b38a4ba update trojan server 2020-06-11 21:50:54 +08:00
a1ae4caee3 update trojan server 2020-06-11 21:50:18 +08:00
747a3c5c06 update frontend 2020-06-11 21:39:14 +08:00
943304eb02 support trojan and more optimization 2020-06-11 20:47:02 +08:00
9b82df98f5 update 2020-06-08 01:08:07 +08:00
3df4e04605 update online user stat 2020-06-05 13:38:17 +08:00
eace40eb08 update online user stat 2020-06-05 13:30:38 +08:00
49b60f7c23 update online user stat 2020-06-05 13:26:22 +08:00
47a6c4077a update online user stat 2020-06-05 13:25:32 +08:00
3200c427ce update cacheServerStat 2020-06-05 01:16:27 +08:00
a40a1ab674 update cacheServerStat 2020-06-05 01:13:45 +08:00
397436761f update server stat 2020-06-05 00:52:17 +08:00
0fc9b1d867 update server stat and app default config 2020-06-05 00:28:14 +08:00
d6b22011ba update server stat and app default config 2020-06-05 00:25:15 +08:00
78f8bd6906 support stripe credit card 2020-06-04 19:17:47 +08:00
2e64b3873c support stripe credit card 2020-06-04 18:59:02 +08:00
022df26134 stripe test 2020-06-04 18:39:54 +08:00
54e59f1178 stripe test 2020-06-04 18:33:44 +08:00
5982f3349c stripe test 2020-06-04 18:29:41 +08:00
beb2c90321 stripe test 2020-06-04 18:28:47 +08:00
036bb00c08 stripe test 2020-06-04 18:26:28 +08:00
b001f54b84 stripe test 2020-06-04 18:12:53 +08:00
15a3f16b9f stripe test 2020-06-04 18:06:18 +08:00
5d413fac55 stripe test 2020-06-04 18:02:19 +08:00
136cffcf13 stripe test 2020-06-04 18:00:33 +08:00
ca04634537 possible change order issues 2020-06-02 03:11:31 +08:00
288bd43d44 fix search 2020-06-02 03:02:07 +08:00
26df97a862 opt 2020-06-02 01:43:22 +08:00
95e698dbc8 Merge pull request #255 from v2board/dev
1.3
2020-06-01 23:06:52 +08:00
c3e924036a fix dashboard month income 2020-06-01 01:02:45 +08:00
8dc19ee67b fix telegram webhook use frontend url 2020-05-31 15:15:20 +08:00
d020ddf926 fix 2020-05-30 19:52:43 +08:00
334f70f19e fix 2020-05-30 19:49:57 +08:00
49e155797a set log level in global 2020-05-30 14:48:09 +08:00
53e1e41902 set log level in global 2020-05-30 14:43:18 +08:00
afc9b462e5 fetch 2020-05-30 00:09:24 +08:00
5c2c7e502c fix ticket user edit 2020-05-29 13:35:29 +08:00
c3eac30c66 Merge pull request #246 from ColetteContreras/dev
Save original traffic to log
2020-05-29 01:32:03 +08:00
f667f5ee41 fix 2020-05-28 23:28:58 +08:00
8838381432 Save original traffic to log 2020-05-28 17:05:36 +08:00
a6bcc3f153 fix 2020-05-28 16:10:43 +08:00
593066f9de fix 2020-05-28 16:00:57 +08:00
73cbfba19b fix pooling 2020-05-28 14:49:10 +08:00
17e3905f18 fix 2020-05-28 14:28:10 +08:00
b7e8db727c Merge pull request #242 from betaxab/patch-1
rules: let's support custom surge/surfboard config
2020-05-27 17:30:30 +08:00
7464466b85 rules: let's support custom surge/surfboard config
clash rules up-to-date
rename default.surge2.conf to default.surfboard.conf
surfboard rules up-to-date

Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-05-27 10:35:03 +08:00
417d5255e7 Merge pull request #240 from betaxab/patch-1
rules: let's support surge3 configuration
2020-05-26 15:48:09 +08:00
9885661795 fix commission pedding stat 2020-05-26 15:46:44 +08:00
bade7e2cf3 change clash config 2020-05-26 15:34:29 +08:00
88c0e75937 rules: let's support surge3 configuration
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-05-26 09:24:40 +08:00
7eb5532c30 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-05-25 18:02:46 +08:00
eb49e29cd0 gmail limit 2020-05-25 18:02:13 +08:00
6a5c3f6206 Merge pull request #238 from ColetteContreras/dev
Update poseidon server config
2020-05-25 17:43:25 +08:00
ccd52546c8 Merge pull request #210 from betaxab/patch-2
Improved tls support
2020-05-25 17:42:27 +08:00
d9b4a872ff Merge pull request #218 from betaxab/patch-3
Support Surge/Surfboard One-click subscribe in the Tutorial
2020-05-25 17:42:00 +08:00
6c3148bdb9 update order show 2020-05-25 17:38:19 +08:00
afcd0d9c10 update telegram & user edit 2020-05-25 17:20:48 +08:00
b851f1207a update telegram 2020-05-25 16:41:05 +08:00
2e13bab3d8 update telegram 2020-05-25 16:39:35 +08:00
bd1b339db8 update telegram 2020-05-25 16:18:37 +08:00
0a32b0b085 update telegram 2020-05-25 16:04:21 +08:00
c90aa538bc update telegram 2020-05-25 16:01:52 +08:00
506d662ae9 update telegram 2020-05-25 15:50:10 +08:00
dab9afca53 fix admin change password 2020-05-25 15:48:12 +08:00
6e509dab73 fix admin change password 2020-05-23 23:21:49 +08:00
9ea13bb00f Update poseidon server config 2020-05-23 15:08:33 +08:00
59672a6f2c update change plan update transfer 2020-05-23 01:38:27 +08:00
71c42765fc update telegram 2020-05-20 18:33:54 +08:00
c5b56da958 update telegram 2020-05-20 16:19:29 +08:00
83592d2f3f update telegram 2020-05-20 16:15:37 +08:00
941289c641 update telegram 2020-05-20 16:11:18 +08:00
e3c5466c0a update telegram 2020-05-20 15:58:27 +08:00
c5b5abab1a update telegram 2020-05-20 15:57:38 +08:00
03eb8d0724 update telegram 2020-05-20 15:53:23 +08:00
1ed5a278da update telegram 2020-05-20 15:51:32 +08:00
108d54f3cb update telegram 2020-05-20 15:30:51 +08:00
c648308634 update telegram 2020-05-20 11:56:56 +08:00
12db88b998 update telegram 2020-05-20 02:16:49 +08:00
11ca911d02 update telegram 2020-05-20 02:12:18 +08:00
de77170bdc update telegram 2020-05-20 01:25:30 +08:00
f030023ec0 update telegram 2020-05-19 17:00:33 +08:00
fddd816129 update telegram 2020-05-19 16:49:33 +08:00
fa6aea6e2d update telegram 2020-05-19 16:45:54 +08:00
ce19ebc97f update telegram 2020-05-19 16:32:35 +08:00
ca650dd067 update mysql 2020-05-18 12:26:39 +08:00
1acfd84d4b update telegram 2020-05-17 16:01:48 +08:00
29d7228861 update telegram 2020-05-17 16:00:28 +08:00
422b18ca66 update telegram 2020-05-17 15:23:39 +08:00
871291e02d update commission 2020-05-16 01:32:22 +08:00
f26d9495e3 update commission 2020-05-13 16:01:09 +08:00
a7d6b615ed update commission 2020-05-13 12:45:22 +08:00
8afa3c8f09 update 2020-05-12 23:57:28 +08:00
503ac97a8c update 2020-05-12 23:53:48 +08:00
ade3770d50 update 2020-05-12 21:24:18 +08:00
83cbe86192 update gateway name 2020-05-12 20:03:41 +08:00
22643f04b1 fix order assign 2020-05-12 10:08:13 +08:00
af71ab8e27 update 2020-05-11 18:27:36 +08:00
c29bd836eb update 2020-05-11 18:26:16 +08:00
0c2090cb3c update 2020-05-11 17:43:08 +08:00
f7959dcd93 update 2020-05-11 17:19:58 +08:00
9158697546 order assign 2020-05-11 15:50:02 +08:00
c6cfa2d31c order assign 2020-05-11 15:34:53 +08:00
aaa04f12a3 order assign 2020-05-11 15:20:32 +08:00
a4df1416de order assign 2020-05-11 15:19:52 +08:00
d1a2e7a29e order assign 2020-05-11 02:13:20 +08:00
a3b400ed32 update 2020-05-10 23:10:52 +08:00
0d6d10421b ticket notify 2020-05-10 23:09:22 +08:00
deb12ae707 ticket notify 2020-05-10 21:20:51 +08:00
bf3b7bb66f ticket notify 2020-05-10 19:04:16 +08:00
15a28e7bd3 transfer and withdraw 2020-05-10 18:38:02 +08:00
c46b8b1b40 fix 2020-05-07 18:54:18 +08:00
98b9ca62f6 opt ui 2020-05-05 14:28:01 +08:00
6857967eec fix ticket 2020-05-05 12:57:08 +08:00
9fed8b8f9d fix commission balance 0 show 2020-05-04 23:01:22 +08:00
c5d74f8b38 fix 2020-05-04 20:34:55 +08:00
3167306bc8 add auto check commission 2020-05-04 20:08:43 +08:00
51fee8892e order process fix 2020-05-04 17:16:47 +08:00
25d4c5b31d order process fix 2020-05-04 17:04:21 +08:00
aaad8a7f7e order process fix 2020-05-04 17:03:31 +08:00
5630066aa4 order process fix 2020-05-04 16:44:40 +08:00
998ac1d500 auto check commission 2020-05-03 23:17:55 +08:00
890626d892 fix plan off, buy reset package 2020-05-03 19:42:46 +08:00
dae5d2a2a3 update 2020-05-02 14:29:04 +08:00
c7a45c9d3d update 2020-04-30 16:08:26 +08:00
71a8daf271 tutorial: add a surge ue_subscribe_url variable
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-04-29 23:22:03 +08:00
8fd0592139 update 2020-04-26 19:33:56 +08:00
153a0e9fdf update 2020-04-26 19:32:42 +08:00
9f3aaac614 update 2020-04-26 19:21:14 +08:00
ba1c4ffa00 update 2020-04-26 19:19:31 +08:00
99117cca58 update 2020-04-26 19:15:13 +08:00
42ad99065e update 2020-04-26 14:52:57 +08:00
1da49f7f9f update 2020-04-26 14:46:55 +08:00
16bdafc952 update 2020-04-26 12:50:03 +08:00
ecca13911c update 2020-04-26 12:24:26 +08:00
8a0ac687cf update 2020-04-25 19:55:59 +08:00
2e4de78923 update 2020-04-25 19:45:15 +08:00
867f1760d3 update 2020-04-25 19:44:47 +08:00
e2a3a1e72d subscription: Improved TLS support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-04-24 11:42:24 +08:00
c17b614e13 update 2020-04-22 16:57:00 +08:00
45a76b25ef update 2020-04-21 01:30:19 +08:00
3f2c8266de update 2020-04-21 00:22:05 +08:00
94158cb6e3 update 2020-04-20 16:26:23 +08:00
467f33c71d update 2020-04-20 16:20:55 +08:00
2076dded41 update 2020-04-20 16:18:21 +08:00
9e07861c9b update 2020-04-20 16:12:39 +08:00
6e2379cb6b update 2020-04-20 16:10:41 +08:00
295b4552d7 update 2020-04-20 16:09:18 +08:00
4ccd41e197 update 2020-04-20 16:08:33 +08:00
3b486e4693 update 2020-04-20 16:07:06 +08:00
50b5ed6b8e update sql 2020-04-16 23:01:18 +08:00
39ae037080 update sql 2020-04-14 01:10:18 +08:00
89b6fe119f update sql 2020-04-13 19:43:36 +08:00
bb56b581be fix rules 2020-04-13 17:28:59 +08:00
1cc0dea454 update refund process 2020-04-11 23:13:16 +08:00
4301d7e4ab update refund process 2020-04-09 13:35:27 +08:00
2523253637 clear update sql 2020-04-08 23:19:01 +08:00
1b3833173d fix dns port string to int 2020-04-08 15:35:01 +08:00
54a8542e0f fix dns port string to int 2020-04-07 17:55:15 +08:00
3e550142cd update dns 2020-04-07 02:47:12 +08:00
b020f2c196 Merge pull request #173 from v2board/dev
1.2.5
2020-04-07 01:36:38 +08:00
e1b16ef7e6 1.2.5 2020-04-07 01:35:03 +08:00
887aad7737 Merge pull request #172 from v2board/dev
#170
2020-04-07 01:20:20 +08:00
a1f2290ff2 #170 2020-04-07 01:19:52 +08:00
d23daf4a68 Merge pull request #171 from v2board/dev
1.2.5
2020-04-07 01:14:10 +08:00
c4868a9c48 send mail delay && fetch admin 2020-04-07 01:10:14 +08:00
6050e6e4a9 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-04-07 00:41:19 +08:00
7c69e19304 #170 2020-04-07 00:40:52 +08:00
fd42a855cf Merge pull request #166 from betaxab/patch-1
rules: fixes surge ws host headers & allow insecure tls
2020-04-07 00:36:13 +08:00
e73cbe9597 fix dns not active 2020-04-06 18:05:58 +08:00
276b040581 rules: fixes surge ws host headers & allow insecure tls
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-04-06 12:46:16 +08:00
495a5f89c5 Merge pull request #165 from v2board/dev
1.2.4
2020-04-05 15:32:38 +08:00
7c3309164b fix remindexpire 2020-04-05 15:30:58 +08:00
674b31675a fix remindtraffic 2020-04-05 15:29:45 +08:00
b2c33cd31b Merge pull request #149 from SquidFerry/hotfix/password
fix:change password
2020-04-05 15:18:28 +08:00
4240e8355a fix stripe not active 2020-04-05 15:17:34 +08:00
7770bf6b99 Merge pull request #162 from betaxab/patch-1
rules: fix HTTPS variable not set for some servers
2020-04-05 15:12:35 +08:00
55118d7706 rules: fix HTTPS variable not set for some servers
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-04-05 01:22:56 +08:00
2b34f5ec82 1.2.4 2020-04-03 13:28:44 +08:00
7dff7ddfc7 update 2020-04-02 22:32:35 +08:00
ed3e468a0a 1.2.4 2020-04-02 22:00:24 +08:00
901d89b5d7 1.2.4 2020-04-02 21:59:57 +08:00
402b9e0c3f 1.2.4 2020-04-02 17:18:47 +08:00
ae543d1c2c 1.2.4 2020-04-02 15:01:33 +08:00
5d9b98f383 Merge pull request #154 from betaxab/patch-1
修复换行
2020-04-02 15:01:01 +08:00
a2183b7143 rules: fix surge config text wrapping
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-04-02 12:42:07 +08:00
a2278487ee 1.2.4 2020-04-01 02:15:07 +08:00
075f7b39a8 1.2.4 2020-04-01 00:51:45 +08:00
k
3db93b4739 fix:change password 2020-03-31 10:47:14 +08:00
1fc9f94dad 1.2.4 2020-03-31 01:46:33 +08:00
bdfa1ff0d5 1.2.4 2020-03-31 01:27:04 +08:00
98e4aca61f 1.2.4 2020-03-31 01:18:46 +08:00
139aeb3f48 update 2020-03-31 00:36:01 +08:00
dfdf995ddb update 2020-03-31 00:34:25 +08:00
4b4d777a4e update 2020-03-31 00:23:09 +08:00
42607a789d update 2020-03-31 00:22:49 +08:00
79f53f2836 update 2020-03-31 00:22:07 +08:00
d1bf743316 update 2020-03-30 23:47:54 +08:00
39fadd8a63 update 2020-03-30 23:40:55 +08:00
4831c9f194 Merge pull request #143 from betaxab/patch-1
rules: add surge/surfboard support [1/2]
2020-03-30 22:44:29 +08:00
ee80e0f2ff update 2020-03-30 16:11:08 +08:00
1be7151b6c update 2020-03-30 00:28:10 +08:00
bb1ad02cf8 update 2020-03-30 00:27:52 +08:00
64f379d99d update 2020-03-30 00:10:15 +08:00
c271647ecc update 2020-03-29 23:57:34 +08:00
e8b6f1b481 update 2020-03-29 23:49:06 +08:00
1c6907fe33 rules: add surge/surfboard support [1/2]
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-03-28 11:27:16 +08:00
68f7cdeed8 Merge pull request #130 from v2board/dev
Dev
2020-03-21 19:58:19 +08:00
40e5ee62b1 Merge pull request #129 from DesperadoJ/dev
Minor bug fixes
2020-03-21 19:57:54 +08:00
9f9bb14e9d Fix email template typo 2020-03-21 19:50:49 +08:00
bcc80581d3 Fix Quantumult sub 2020-03-21 19:50:29 +08:00
288f4aba18 Merge pull request #127 from v2board/dev
fix quantumult sub
2020-03-21 16:02:07 +08:00
cc673cdbd1 fix quantumult sub 2020-03-21 16:00:15 +08:00
0781a0740b Merge pull request #126 from v2board/dev
1.2.3
2020-03-21 15:34:35 +08:00
8d56db2bf6 update user ver 2020-03-21 15:33:36 +08:00
56fd3f5b99 Merge pull request #125 from v2board/dev
1.2.3
2020-03-21 15:09:16 +08:00
5a84e412c4 fix ticket 2020-03-21 15:04:38 +08:00
3216a90235 fix ticket 2020-03-21 00:27:58 +08:00
59fa3a3316 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-03-20 23:15:01 +08:00
06465b3eb3 update email template 2020-03-20 23:13:53 +08:00
cc12605984 Merge pull request #123 from ColetteContreras/dev
Use ServerService
2020-03-20 17:51:15 +08:00
8fa9d60c4d update email template 2020-03-20 13:57:09 +08:00
1c3eff241a update email template 2020-03-20 13:51:29 +08:00
79aa942d5b update email template 2020-03-19 15:54:08 +08:00
f4688b6d50 update email template 2020-03-19 15:52:39 +08:00
d9c0d18689 update email template 2020-03-19 15:41:21 +08:00
7e88a39249 Use ServerService 2020-03-19 15:25:38 +08:00
064834001d update 2020-03-18 18:16:33 +08:00
49d5b407bc update 2020-03-18 18:12:19 +08:00
1291bf47be update 2020-03-18 17:33:58 +08:00
e15d5961f0 update 2020-03-18 16:01:10 +08:00
04c6b865b4 update sql 2020-03-17 22:16:12 +08:00
5b33bf7a0b update 2020-03-17 22:09:06 +08:00
2235a5e7c5 fix 2020-03-17 21:21:35 +08:00
6fba0b6dab add balance payment 2020-03-17 20:16:55 +08:00
3075f0d411 add balance payment 2020-03-17 20:04:00 +08:00
111d2720bd add balance payment 2020-03-17 20:00:46 +08:00
03e0b5d087 fix client onetime 2020-03-17 19:00:33 +08:00
96f562a9e7 fix elq update try catch 2020-03-17 14:28:47 +08:00
7bb4852cca update 2020-03-17 01:56:44 +08:00
8986ba1d42 update 2020-03-17 01:50:50 +08:00
f193f35642 update 2020-03-15 21:32:12 +08:00
2a92ee8b41 update 2020-03-15 21:28:57 +08:00
56cabbdc00 update 2020-03-15 21:26:56 +08:00
5f4d02dde3 fix 2020-03-15 20:30:00 +08:00
00c2dee361 opt 2020-03-15 20:21:31 +08:00
35917ad199 fix reset traffic 2020-03-15 20:12:52 +08:00
e4cb6458c0 update 2020-03-14 15:33:31 +08:00
93c1031078 update 2020-03-13 14:42:47 +08:00
26252aee02 update 2020-03-13 14:37:28 +08:00
8d10b52a35 update 2020-03-13 14:37:15 +08:00
6e7da97fcd update 2020-03-13 14:33:48 +08:00
13dbb143f8 update send email verify ttl 300 sec 2020-03-13 14:32:36 +08:00
01da63f82e pr #109 2020-03-13 13:47:20 +08:00
d2a0422f64 pr #109 2020-03-13 13:47:02 +08:00
c81cb8acca Is the commission only paid at the first time 2020-03-13 01:52:34 +08:00
6bf0d2d94e opt 1.2.3 2020-03-11 19:25:07 +08:00
260c1d7361 opt 1.2.3 2020-03-10 21:31:53 +08:00
6a6de2dc22 opt 1.2.3 2020-03-10 21:31:18 +08:00
ba9ec7006b opt 1.2.3 2020-03-10 21:13:41 +08:00
f17b5d04a8 opt 1.2.3 2020-03-10 21:10:44 +08:00
cccd8f36ee opt 1.2.3 2020-03-10 14:03:48 +08:00
4d7ebe4aea opt 1.2.3 2020-03-10 13:56:24 +08:00
8ac8427c2f opt 1.2.3 2020-03-10 13:47:50 +08:00
97056be8c3 opt 1.2.3 2020-03-10 13:45:48 +08:00
68d44e7657 opt 1.2.3 2020-03-10 13:39:05 +08:00
b07511f01b opt 1.2.3 2020-03-10 13:30:30 +08:00
a13809ac02 opt 1.2.3 2020-03-10 13:27:04 +08:00
5b317478c6 opt 1.2.3 2020-03-10 13:11:31 +08:00
57fd282024 fix client not use token login 2020-03-08 16:52:09 +08:00
27ccf9869d Merge pull request #95 from v2board/dev
Dev
2020-03-07 11:50:15 +08:00
5e5e3fbb08 1.2.2 2020-03-07 11:48:24 +08:00
f5132abad1 1.2.2 2020-03-07 11:37:39 +08:00
4f709bf1f6 1.2.2 2020-03-06 21:58:50 +08:00
e62ef5cb0a 1.2.2 2020-03-06 16:39:37 +08:00
e5c207ccff 1.2.2 2020-03-06 16:23:49 +08:00
6b7cea671d 1.2.2 2020-03-06 16:21:08 +08:00
dbf73f3a38 Merge pull request #81 from betaxab/patch-1
clash: Update default clash rules from latest SS-Rule-Snippet
2020-03-06 13:52:59 +08:00
a3b7130857 Merge pull request #80 from v2board/dev
Dev
2020-03-06 01:22:12 +08:00
c0bae87556 Merge branch 'master' of https://github.com/v2board/v2board into dev 2020-03-06 01:20:19 +08:00
f18715ed09 update 2020-03-06 01:19:51 +08:00
7f3e0d9fb9 clash: Update default clash rules from latest SS-Rule-Snippet
Complete the telegram IP address
2020-03-06 01:19:41 +08:00
e5c8e18206 Merge pull request #79 from v2board/dev
Dev
2020-03-06 01:10:17 +08:00
796a5ba55e Merge pull request #75 from daydaypy/dev
Stripe invoice description
2020-03-06 01:07:06 +08:00
9bf6f64f71 1.2.1 2020-03-06 01:06:04 +08:00
Aaa
66957eff6d Stripe invoice description 2020-03-05 23:41:44 +08:00
4e03662e8f fix pay form verify 2020-03-05 23:05:36 +08:00
fc48f5553f 1.2.1 2020-03-05 22:11:12 +08:00
c4ddde1c94 1.2.1 2020-03-05 22:10:35 +08:00
92f44d7a2e 1.2.1 2020-03-05 22:04:18 +08:00
3bfd64d8ca 1.2.1 2020-03-05 16:55:01 +08:00
cb2efac160 fix 2020-03-05 16:28:20 +08:00
c3898ec795 fix 2020-03-05 16:19:32 +08:00
6dc5dd0edb Merge pull request #73 from v2board/dev
Dev
2020-03-05 03:35:46 +08:00
e97f6c64a0 fix 2020-03-05 03:34:38 +08:00
89630d889d fix 2020-03-05 03:33:21 +08:00
3becb71a5a fix 2020-03-05 03:32:25 +08:00
d361ff80ed Merge pull request #72 from v2board/dev
Dev
2020-03-05 03:20:14 +08:00
3fe442313d fix 2020-03-05 03:19:46 +08:00
f76609a38d fix 2020-03-05 03:07:17 +08:00
51f4ad417e fix tags 2020-03-05 02:30:02 +08:00
516b2626ae Merge pull request #71 from v2board/dev
1.2
2020-03-05 02:16:29 +08:00
b9312f362c pre 1.2 2020-03-05 02:03:50 +08:00
102f18bef1 pre 1.2 2020-03-05 00:10:52 +08:00
1fc36d8e29 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-03-05 00:07:10 +08:00
a3837f83cb support stripe currency 2020-03-05 00:06:40 +08:00
47f27f4852 Merge pull request #66 from ColetteContreras/dev
Update poseidon
2020-03-04 22:14:09 +08:00
e73eaea66c pre 1.2 2020-03-04 21:52:31 +08:00
bc5ddbf40a update 2020-03-04 21:45:06 +08:00
a2c674f00c update 2020-03-04 19:34:47 +08:00
218338960b update 2020-03-04 19:33:33 +08:00
d9dcea4e4f update 2020-03-03 01:26:14 +08:00
fad12a62ce update 2020-03-03 01:21:11 +08:00
1ee104ec88 fix 2020-03-03 01:00:44 +08:00
19dcd0ffba fix 2020-03-03 00:56:31 +08:00
ab2e39f7dd fix 2020-03-02 23:36:19 +08:00
8f4ebe1764 fix 2020-03-02 23:21:27 +08:00
3763320261 fix 2020-03-02 20:47:52 +08:00
376c79aa91 fix 2020-03-02 20:32:15 +08:00
47686d50c5 fix 2020-03-02 20:29:17 +08:00
c8cb1f8e83 fix 2020-03-02 17:58:07 +08:00
620ed8e04c update 2020-03-02 15:40:19 +08:00
c28a6ec1d3 fix user plan not nullable 2020-03-02 15:08:37 +08:00
3c78bed800 fix user plan not nullable 2020-03-02 15:05:43 +08:00
72fb0d131d update 2020-03-02 00:36:15 +08:00
378c96d4c7 remove 2020-03-02 00:33:10 +08:00
ca2a744b10 opt config && add renew reset traffic 2020-03-02 00:27:06 +08:00
0f72e9a091 add renew reset traffic switch 2020-03-02 00:14:58 +08:00
bb49fb15d1 update reset traffic method 2020-03-02 00:04:07 +08:00
47d8dfd7c8 update onetime package 2020-03-01 23:29:49 +08:00
60b197410c update onetime package 2020-03-01 23:18:45 +08:00
e2b73094c0 fix rules message 2020-03-01 17:57:50 +08:00
7d8b8ad8ac update frontend 2020-03-01 16:25:15 +08:00
1992b0a9e9 Update poseidon 2020-02-29 17:00:28 +08:00
b4bede869d fix 2020-02-29 15:22:01 +08:00
a11e65f84a fix null is not post 2020-02-28 20:06:50 +08:00
e1a8ffe7c4 update admin frontend 2020-02-28 17:18:07 +08:00
2217286d03 fix mail send 2020-02-28 17:16:05 +08:00
dd924d95c6 update user manage 2020-02-28 16:38:44 +08:00
53710f2e01 update user manage 2020-02-28 16:35:40 +08:00
f03c53815c rewrite banned logic 2020-02-28 16:25:25 +08:00
6e98e42f51 rewrite banned logic 2020-02-28 16:21:16 +08:00
9932e34634 fix replace the negative subscription problem 2020-02-28 16:14:25 +08:00
6ba4702539 fix replace the negative subscription problem 2020-02-28 16:11:26 +08:00
b29ac8ac2c fix plan manage price not set 2020-02-28 15:30:06 +08:00
866fda84ed add onetime plan 2020-02-28 15:16:23 +08:00
95c7c48cc7 add onetime plan 2020-02-28 15:06:30 +08:00
010f0cbc03 add onetime plan 2020-02-28 15:04:13 +08:00
0d2b8bc976 add onetime plan 2020-02-28 14:53:33 +08:00
3b9e9cbe7f add onetime plan 2020-02-28 14:51:06 +08:00
a7b47d8f77 add onetime plan 2020-02-28 12:52:29 +08:00
dea26121fc add onetime plan 2020-02-28 01:22:16 +08:00
10e4de39de add onetime plan 2020-02-28 01:21:14 +08:00
e7fd81bf4c add onetime plan 2020-02-28 01:12:55 +08:00
0f9cb9696d add onetime plan 2020-02-28 01:03:05 +08:00
a7b3d6e778 add onetime plan 2020-02-27 19:39:26 +08:00
637bacd62f add onetime plan 2020-02-27 19:36:30 +08:00
2171ef59d7 fix tutorial 2020-02-25 09:56:06 +08:00
0a92308ad2 update plan 2020-02-24 02:04:42 +08:00
8ff53673d7 Merge pull request #55 from DesperadoJ/patch-1
Refine code
2020-02-24 01:46:16 +08:00
07772ceb66 fix f2f show qrcode 2020-02-23 22:45:06 +08:00
fc8333d757 Refine code
Remove duplicates
2020-02-23 22:15:31 +08:00
64dbd11e62 support clash for android subscribe 2020-02-23 20:02:34 +08:00
bfeab8eae2 fix server rules empty 2020-02-23 17:20:24 +08:00
5ee6fc2996 opt 2020-02-23 02:37:52 +08:00
018e4aa810 fix commission status update 2020-02-23 02:05:23 +08:00
17d6d04cc6 update frontend models && throttle 120:1 2020-02-22 20:23:45 +08:00
e174ef193b update tutorial 2020-02-22 14:56:13 +08:00
1887259554 reconsitution frontend && update tutorial 2020-02-22 14:39:33 +08:00
8b804913f5 update tutorial 2020-02-19 14:59:38 +08:00
a2ea88beb5 update tutorial 2020-02-19 14:33:35 +08:00
d969220654 update tutorial 2020-02-19 14:32:22 +08:00
5154993887 update tutorial 2020-02-19 14:31:02 +08:00
61b31c1925 update tutorial 2020-02-18 22:48:00 +08:00
a0454577cb update tutorial 2020-02-18 22:46:44 +08:00
f7fc6b9dfc update tutorial 2020-02-18 22:44:56 +08:00
1b7fc9529f update tutorial 2020-02-18 22:28:17 +08:00
a56faa7e8e update tutorial 2020-02-18 22:06:54 +08:00
0a2c626f89 update tutorial 2020-02-18 21:46:00 +08:00
19df032b57 fix email whitelist 2020-02-18 18:17:12 +08:00
de8f95ee3d fix ui 2020-02-18 17:57:25 +08:00
568648dc2f update change order process 2020-02-18 14:52:26 +08:00
9f5be4e83a update ui 2020-02-18 14:45:05 +08:00
14d0de18ec update change order process 2020-02-18 13:52:10 +08:00
64ae39aa2d update change order process 2020-02-18 12:42:05 +08:00
fb09baa3c1 update change order process 2020-02-18 03:28:25 +08:00
153bdcaad1 remove downgrade 2020-02-18 02:55:44 +08:00
f3ac8a37be update downgrade 2020-02-18 02:40:00 +08:00
9925ab6b47 update downgrade 2020-02-18 02:35:42 +08:00
84ff8d3ab3 update config 2020-02-18 02:33:57 +08:00
fcec3af75f Merge pull request #45 from v2board/dev
Dev
2020-02-17 23:13:56 +08:00
2a9a8805a8 update sql 2020-02-17 23:12:00 +08:00
72eafb9698 Merge pull request #44 from DesperadoJ/dev
Fix typo, update Clash config & v2ray server config
2020-02-17 23:10:16 +08:00
e837e774ef Update v2ray server config
Enable "sniffing".
Refine "routing".
2020-02-17 22:39:51 +08:00
00fcff5f0d Update Clash config
Add "skip-cert-verify: true" for self-signed certifcate
2020-02-17 22:17:16 +08:00
6b43597eaf Fix typo 2020-02-17 22:12:47 +08:00
c576299a60 Merge pull request #43 from v2board/dev
1.1.2
2020-02-17 22:06:10 +08:00
4926264160 update ui 2020-02-17 22:03:00 +08:00
0c1a53faab update ui 2020-02-17 21:34:06 +08:00
0f0b092f86 update ui 2020-02-17 21:29:53 +08:00
3beb938cbc update ui 2020-02-17 14:01:30 +08:00
c45578417f Merge pull request #42 from ColetteContreras/master
Update poseidon
2020-02-17 13:46:33 +08:00
088dc2cfa6 update ui 2020-02-17 13:41:19 +08:00
0b4c884824 update ui 2020-02-17 12:42:41 +08:00
7b9390a6cb update ui 2020-02-17 12:35:50 +08:00
2b69e83132 update order change process 2020-02-17 12:25:19 +08:00
56820346d0 Update poseidon 2020-02-17 11:30:58 +08:00
1ea307854f update theme 2020-02-17 03:55:38 +08:00
290ef77c03 update theme 2020-02-17 03:53:33 +08:00
172a639f59 update theme 2020-02-17 03:51:06 +08:00
bff92180b0 update theme 2020-02-17 03:37:04 +08:00
e094a2f4d1 update theme 2020-02-17 03:35:03 +08:00
b852180b4d update order change process 2020-02-17 01:43:18 +08:00
a0a5327f42 update order change process 2020-02-17 01:31:39 +08:00
874648a4c0 update order change process 2020-02-17 01:27:07 +08:00
15cd13c26c update order change process 2020-02-17 01:00:12 +08:00
76c50e01bf update order change process 2020-02-17 00:42:56 +08:00
9ae3994705 update order change process 2020-02-17 00:39:22 +08:00
cab7cc9d19 update order change process 2020-02-17 00:35:21 +08:00
dbac9d9dac Merge pull request #38 from v2board/dev
1.1.1
2020-02-16 18:39:47 +08:00
2e18b1b108 1.1.1 2020-02-16 18:39:10 +08:00
c1583a1014 Merge pull request #37 from v2board/dev
1.1.1
2020-02-16 17:54:29 +08:00
01c5901c71 1.1.1 2020-02-16 17:52:14 +08:00
0446e7d99f fix server rules maybe not defined 2020-02-16 16:51:25 +08:00
557f6bc0c2 fix admin login 2020-02-16 14:05:38 +08:00
e49dc87fda fix admin login 2020-02-16 13:55:48 +08:00
edf88cbccf fix admin login 2020-02-16 13:52:15 +08:00
19940f599d fix admin login 2020-02-16 13:48:31 +08:00
ace06630d8 fix commission frontent 2020-02-16 00:28:39 +08:00
50582137e4 update frontend 2020-02-15 23:27:59 +08:00
ab8da6bb02 update user fetch 2020-02-15 23:16:44 +08:00
0a58930619 compatibility old client 2020-02-15 23:04:50 +08:00
9d9866689d update order filter 2020-02-15 22:58:31 +08:00
2d21fa3fb7 update user update 2020-02-15 22:40:41 +08:00
5cd020ea7a update mailsend,frontend 2020-02-15 20:32:38 +08:00
43e7bb6c9f update mailsend,frontend 2020-02-15 17:24:00 +08:00
2b8456fdf5 update discount 2020-02-15 00:55:04 +08:00
b34fa70165 update userinfo fetch 2020-02-14 22:11:13 +08:00
d845439f14 add user discount 2020-02-14 21:54:16 +08:00
d356722482 update frontend 2020-02-14 18:24:59 +08:00
72ac20d387 update frontend 2020-02-13 17:09:11 +08:00
878a7da62c update frontend 2020-02-13 17:02:37 +08:00
4fd0c9ce51 update frontend 2020-02-13 16:58:48 +08:00
18ed6947b3 update frontend 2020-02-13 16:54:15 +08:00
b8b1e1043b update frontend 2020-02-13 16:45:54 +08:00
705ceb80b2 update frontend 2020-02-13 16:24:12 +08:00
47be10f274 update frontend 2020-02-13 16:22:30 +08:00
540b331f16 fix forget 2020-02-13 13:44:58 +08:00
e9ba149c0f update sql 2020-02-13 13:43:55 +08:00
895e0f177b fix description not show 2020-02-12 23:36:40 +08:00
9dc64114f5 opt paytaro error report 2020-02-12 14:01:40 +08:00
168b0ad72e opt paytaro error report 2020-02-12 14:01:00 +08:00
c82acafdf8 update api 2020-02-11 21:06:15 +08:00
e14489cf66 update api 2020-02-11 21:04:47 +08:00
96a36b1afb update ui 2020-02-11 17:12:57 +08:00
df9d99c271 update ui 2020-02-11 17:09:27 +08:00
79bd46c0da update ui 2020-02-11 16:35:12 +08:00
e638660bf7 update sql 2020-02-11 16:23:38 +08:00
880b8e8ad9 update ui 2020-02-11 16:21:36 +08:00
ba40e3a1bf fix order renew 2020-02-11 15:45:30 +08:00
8fafdc9bb5 fix order renew 2020-02-11 14:03:23 +08:00
e07b97b14e fix order renew 2020-02-11 13:31:58 +08:00
c817710556 fix order renew 2020-02-11 13:11:15 +08:00
19fd19fbcf add server rules 2020-02-10 21:24:49 +08:00
26e63cbe1c add server rules 2020-02-10 18:54:17 +08:00
5bd524fbf1 add server rules 2020-02-10 18:48:06 +08:00
3a7a0c6e62 remove web tls config 2020-02-10 16:11:38 +08:00
23bfb20307 opt tls 2020-02-10 15:52:07 +08:00
7f9ad68e1b opt tls 2020-02-10 15:51:01 +08:00
cb8cdb2e0e fix user sorter 2020-02-09 18:53:45 +08:00
5db26d862b fix get email whitelist suffix 2020-02-09 18:24:24 +08:00
13bc683faf opt register auto login 2020-02-09 18:21:58 +08:00
e9229781bc add email whitelist config 2020-02-09 18:20:33 +08:00
6b9e424b74 add email whitelist config 2020-02-09 18:09:48 +08:00
078e1e7bd6 add email whitelist config 2020-02-09 18:04:40 +08:00
d417e70475 add email whitelist config 2020-02-09 18:02:13 +08:00
214fb2a2bf add email whitelist config 2020-02-09 18:01:06 +08:00
f61e37aeaf add email whitelist config 2020-02-09 17:17:21 +08:00
98e23be297 add email blacklist config 2020-02-09 01:29:26 +08:00
5bc4bff8f9 add sorter 2020-02-09 00:32:39 +08:00
ab97db552d optimization try out 2020-02-08 23:03:48 +08:00
2e2a0bf273 fix try out 2020-02-08 22:26:38 +08:00
1f471545d4 add safe mode 2020-02-08 16:38:03 +08:00
ffe05a5467 optimization install 2020-02-08 16:19:22 +08:00
ec73224671 optimization install 2020-02-08 16:16:12 +08:00
3f4e3d46fb optimization install 2020-02-08 16:09:33 +08:00
6920352c0e optimization install 2020-02-08 16:07:20 +08:00
e57a70970f optimization install 2020-02-08 15:49:59 +08:00
406f1ee80d optimization install 2020-02-08 15:48:25 +08:00
f7558fecc7 optimization install 2020-02-08 15:45:23 +08:00
43d98a485d optimization install 2020-02-08 15:44:30 +08:00
be2caad604 optimization install 2020-02-08 15:43:13 +08:00
984abba50d optimization install 2020-02-08 15:26:18 +08:00
62f7d5b3e8 fix forget 2020-02-07 19:34:24 +08:00
37844c1bb4 fix 2020-02-05 21:09:33 +08:00
87e0d34961 fix tls 2020-02-05 18:46:10 +08:00
ce6bd80702 fix 2020-02-04 00:33:02 +08:00
79bb987c37 fix 2020-02-03 23:02:08 +08:00
ec8357aa3e fix api 2020-02-03 02:47:29 +08:00
24f5324c2a fix client route 2020-02-02 22:06:51 +08:00
0299cd63ad fix client route 2020-02-02 22:02:55 +08:00
bb566f01d9 fix client route 2020-02-02 22:01:50 +08:00
c03bd0098d fix client route 2020-02-02 21:59:12 +08:00
073f7611e1 update 2020-02-02 21:51:49 +08:00
517f7d0122 update clash rule 2020-02-02 21:50:58 +08:00
32d6a983a3 update changepassword 2020-02-02 20:44:52 +08:00
a8db24492b update frontend 2020-02-01 21:54:08 +08:00
1b23998248 split router 2020-02-01 20:58:36 +08:00
d744fba683 split router 2020-02-01 20:57:15 +08:00
3f6799aa4b split router 2020-02-01 20:54:47 +08:00
1d704a4424 split router 2020-02-01 20:51:45 +08:00
ab5aeb545e support multi password hash verify 2020-02-01 00:54:37 +08:00
5e7e42e52c support multi password hash verify 2020-01-31 21:57:43 +08:00
241cbd3016 support multi password hash verify 2020-01-31 21:54:17 +08:00
9ff815f853 update 2020-01-31 21:39:32 +08:00
b61e641dad update 2020-01-31 21:30:44 +08:00
a9eff7bea9 update 2020-01-31 20:04:47 +08:00
fc3c4b3b73 update 2020-01-31 17:54:41 +08:00
b75104475c update 2020-01-31 17:33:55 +08:00
6c606527b1 update 2020-01-31 17:18:00 +08:00
34892d9c34 Merge branch 'master' of https://github.com/v2board/v2board 2020-01-31 17:13:45 +08:00
51bb2779b9 update 2020-01-31 17:13:30 +08:00
45a257778c Merge pull request #25 from ColetteContreras/master
Add Poseidon
2020-01-31 15:18:13 +08:00
4286bbb988 Add Poseidon 2020-01-31 12:50:14 +08:00
2f8f3afebf update 2020-01-31 00:16:00 +08:00
15fc60d3c7 update 2020-01-31 00:03:18 +08:00
b8218f2c51 update 2020-01-30 20:16:14 +08:00
9080de4d0a update 2020-01-30 20:02:12 +08:00
50ffdd9db9 update 2020-01-30 19:59:06 +08:00
6cf3b73da2 update 2020-01-30 19:57:18 +08:00
e963a61910 update 2020-01-30 17:18:25 +08:00
038effdd42 update 2020-01-30 16:59:52 +08:00
5a4b208aab update 2020-01-29 17:10:10 +08:00
af400b3a0b update 2020-01-29 17:08:09 +08:00
9a4b2027f6 update 2020-01-29 17:01:12 +08:00
9ae2fc3f1d update 2020-01-29 16:58:45 +08:00
ba9d00f088 update 2020-01-29 16:49:32 +08:00
7fc34fa2b8 update 2020-01-29 16:42:47 +08:00
7cb5eb03b9 update 2020-01-29 16:37:57 +08:00
d7063191f4 update 2020-01-29 16:22:39 +08:00
deb7e4ed04 update 2020-01-29 16:10:56 +08:00
602e12d745 update 2020-01-29 16:10:21 +08:00
f2ffb184d4 update 2020-01-29 16:09:39 +08:00
6dd562df8c update 2020-01-29 16:08:50 +08:00
5fae1f2bd8 update 2020-01-27 15:51:05 +08:00
a60d68a13f update 2020-01-27 15:49:44 +08:00
dad9e43114 update 2020-01-25 23:04:52 +08:00
aac4ce8098 update 2020-01-20 23:34:53 +08:00
52b9ed47b0 update 2020-01-20 23:04:42 +08:00
6d76e30299 update 2020-01-20 20:12:13 +08:00
0675268a16 update 2020-01-20 17:13:14 +08:00
aaf94e61a2 update 2020-01-20 17:02:19 +08:00
47d5031fe6 update 2020-01-20 17:01:02 +08:00
8d4575ede9 update 2020-01-20 16:47:53 +08:00
e9a69cd0f6 update 2020-01-20 16:43:34 +08:00
625bcc59e8 update 2020-01-20 16:38:34 +08:00
34533ae10c update 2020-01-20 16:28:28 +08:00
b4fc90c133 update 2020-01-20 16:18:12 +08:00
12278e143e update 2020-01-20 16:16:59 +08:00
10510164e4 update 2020-01-20 15:24:48 +08:00
75e91b281c update 2020-01-20 14:47:36 +08:00
c027c1127b update 2020-01-20 14:04:03 +08:00
0dbdb55ed2 update 2020-01-20 14:01:53 +08:00
66042904e8 update 2020-01-20 13:58:50 +08:00
508961c4fb update 2020-01-20 13:57:30 +08:00
f03dbd1dcb update 2020-01-18 18:41:02 +08:00
e576d82955 update 2020-01-17 00:20:54 +08:00
88d7e2d35e update 2020-01-14 19:48:18 +08:00
9b6919e9c9 update 2020-01-14 19:45:19 +08:00
3e86cdcbbf update 2020-01-14 01:16:16 +08:00
9fc4a6129c update 2020-01-14 00:29:59 +08:00
db9743a67f update 2020-01-12 22:38:29 +08:00
0f5942bc03 update 2020-01-12 19:48:13 +08:00
28cc21e09b update 2020-01-12 02:35:40 +08:00
fc7f73b558 update 2020-01-12 02:32:15 +08:00
fb0ebea836 update 2020-01-12 02:16:05 +08:00
7d7cef5f37 update 2020-01-12 02:15:48 +08:00
abd488d247 update 2020-01-12 02:14:43 +08:00
e3916aaa06 update 2020-01-12 02:13:27 +08:00
32795cf541 update 2020-01-12 02:08:06 +08:00
415850184e update 2020-01-12 02:02:45 +08:00
afa3c0aa52 update 2020-01-12 01:53:05 +08:00
82875ae6f0 update 2020-01-12 01:48:01 +08:00
bcd85ef71a update 2020-01-12 01:47:54 +08:00
8f35924dee update 2020-01-12 01:41:49 +08:00
4a01b8f11e update 2020-01-12 01:27:36 +08:00
fc18168aa2 update 2020-01-12 00:49:13 +08:00
abdb6f80af update 2020-01-11 19:18:45 +08:00
fbf3a83104 update 2020-01-11 17:17:25 +08:00
515e23762f update 2020-01-11 15:39:07 +08:00
f7fdfadfb0 format 2020-01-11 13:36:52 +08:00
35f954cd84 update 2020-01-09 12:58:01 +08:00
ed0fe84687 update 2020-01-09 12:57:39 +08:00
a96ca5a363 update 2020-01-08 22:28:16 +08:00
6a051f469e update 2020-01-08 17:40:06 +08:00
5e9bc09396 Merge pull request #12 from ColetteContreras/refactor-cache
Use Cache for varieties of cache server
2020-01-08 17:38:44 +08:00
1ef8eab552 Use Cache for varieties of cache server 2020-01-08 01:34:48 +08:00
4d66941ef6 update 2020-01-06 13:48:58 +08:00
2e505a9759 update 2020-01-06 00:06:34 +08:00
85e0eb2760 update 2020-01-05 23:47:23 +08:00
c68440ce32 update 2020-01-05 23:30:07 +08:00
1cd0dbdd31 update 2020-01-05 23:14:42 +08:00
d268f6ddec Merge pull request #8 from chen-ran/master
Support mailgun
2020-01-05 22:07:02 +08:00
b75c33b1d9 support mailgun 2020-01-05 21:21:54 +08:00
0ee67ef270 update 2020-01-05 16:48:18 +08:00
44e57b7073 update 2020-01-05 15:48:51 +08:00
0de4a137bf update 2020-01-05 15:39:28 +08:00
392c849241 update 2020-01-05 15:35:12 +08:00
2f3f457ad9 update 2020-01-05 02:27:29 +08:00
776e866b3c update 2020-01-05 02:23:26 +08:00
9541bc8cd0 update 2020-01-05 02:16:41 +08:00
dc52c3191c update 2020-01-05 02:07:41 +08:00
85a29d3a8b update 2020-01-05 00:22:41 +08:00
7a0c9ce4c4 update 2020-01-04 18:11:13 +08:00
b4bb93e23e update 2020-01-04 18:08:05 +08:00
11d9654010 update 2020-01-04 17:46:53 +08:00
52163329da update 2020-01-04 17:36:31 +08:00
45b03b4fba update 2020-01-04 17:27:21 +08:00
0749372f34 update 2020-01-04 17:25:44 +08:00
588577d513 update 2020-01-04 17:21:46 +08:00
e3431c6ae7 update 2020-01-03 23:05:14 +08:00
2346b1a2dc update 2020-01-03 23:00:31 +08:00
c61f64d623 update 2020-01-03 22:42:27 +08:00
25fbdf0013 update 2020-01-03 21:29:25 +08:00
3a9b3ab32d update 2020-01-03 21:06:04 +08:00
a524fb6f76 update 2020-01-03 21:04:44 +08:00
2172f088a2 update 2020-01-03 20:55:54 +08:00
93fa074358 update 2020-01-03 20:09:21 +08:00
27e417a5f2 update 2020-01-03 20:04:20 +08:00
97e1d9b3bd update 2020-01-03 20:01:46 +08:00
1a0d8e9c55 update 2020-01-03 19:39:32 +08:00
b49b941e50 update 2020-01-03 19:13:04 +08:00
b9cee36641 update 2020-01-03 19:12:05 +08:00
2ecdc27921 update 2020-01-03 19:09:24 +08:00
2d58744de4 update 2020-01-03 12:38:25 +08:00
869a6a920b update 2020-01-03 12:36:44 +08:00
c82a189d76 update 2020-01-03 12:17:57 +08:00
7543819ef2 update 2020-01-03 01:47:15 +08:00
4ceca1957f update 2020-01-03 01:45:58 +08:00
44bd189259 update 2020-01-03 01:12:23 +08:00
b6752e3952 update 2020-01-03 00:45:33 +08:00
45ba9b0c15 update 2020-01-03 00:29:24 +08:00
52e7925cac update 2020-01-03 00:29:15 +08:00
54273a8f16 update 2020-01-03 00:24:45 +08:00
c6461f0bdf update 2020-01-02 23:32:31 +08:00
145f55ae29 update 2020-01-02 23:22:49 +08:00
a201b19940 update 2020-01-02 22:05:27 +08:00
f4166bed45 update 2020-01-02 22:03:24 +08:00
8f8be2ea33 update 2020-01-02 21:55:50 +08:00
df02973756 update 2020-01-02 21:53:02 +08:00
9e55cb2f5d update 2020-01-02 21:52:20 +08:00
5699fe09e9 update 2020-01-02 21:49:25 +08:00
34fa75b4cc update 2020-01-02 21:49:04 +08:00
96d3a27a5b update 2020-01-02 21:39:52 +08:00
b8c8335542 update 2020-01-02 21:38:32 +08:00
aceff450ec update 2020-01-02 13:09:59 +08:00
5d7b5eb8f6 update 2020-01-02 00:43:25 +08:00
afc9d64aab update 2020-01-02 00:21:39 +08:00
09e31dc70b update 2020-01-02 00:20:31 +08:00
d653541ef3 update 2020-01-02 00:19:04 +08:00
dc37499df9 update 2020-01-02 00:11:42 +08:00
2911680eaf update 2020-01-02 00:07:30 +08:00
6ff24f77b8 update 2020-01-02 00:06:23 +08:00
9f4c19bcab update 2020-01-02 00:05:54 +08:00
453a078cd5 update 2020-01-01 23:54:20 +08:00
20be5c3182 update 2020-01-01 23:50:50 +08:00
7639f07b83 update 2020-01-01 23:48:23 +08:00
2a693e4911 update 2020-01-01 23:42:25 +08:00
89d67279c6 update 2020-01-01 23:40:14 +08:00
8bc5004654 update 2020-01-01 18:05:44 +08:00
ec4c0ba339 update 2020-01-01 18:00:45 +08:00
b4e5eb26e3 update 2020-01-01 17:57:06 +08:00
1bf03b28c7 update 2020-01-01 17:55:16 +08:00
aa8f5bfa79 update 2020-01-01 16:35:24 +08:00
495aa04273 update 2020-01-01 16:35:14 +08:00
202a21c17a update 2020-01-01 16:20:44 +08:00
9eefb32a4c update 2020-01-01 15:59:53 +08:00
2eb594a4fa update 2020-01-01 02:25:49 +08:00
34e71ff049 update 2020-01-01 01:50:05 +08:00
63c1faba5e update 2020-01-01 01:36:22 +08:00
5b2e18f702 update 2019-12-31 18:16:43 +08:00
784b53c9f3 update 2019-12-31 18:13:42 +08:00
01d0f6b29d update 2019-12-31 17:49:52 +08:00
2d7d5a564e update 2019-12-31 17:49:24 +08:00
70c1d5c874 update 2019-12-31 15:58:53 +08:00
10029c8362 update 2019-12-31 15:57:12 +08:00
9cd377f8ed update 2019-12-31 15:54:24 +08:00
f5db443668 update 2019-12-31 15:53:27 +08:00
e1fce3ae37 update 2019-12-31 13:45:50 +08:00
fae5ef21e6 update 2019-12-31 01:06:48 +08:00
4c976b1cbe update 2019-12-31 01:05:41 +08:00
6de46f5c36 update 2019-12-31 01:04:55 +08:00
4992ea635c update 2019-12-30 22:52:02 +08:00
928e59a977 update 2019-12-30 22:45:42 +08:00
2be5b8e357 update 2019-12-30 22:45:10 +08:00
76ce89c17f update 2019-12-30 22:40:39 +08:00
8c5b32de90 update 2019-12-30 21:05:48 +08:00
0b89446b63 update 2019-12-30 19:03:15 +08:00
0d4a86c9e9 update 2019-12-30 17:54:50 +08:00
aa1d54137c update 2019-12-30 17:51:18 +08:00
eee6351e35 update 2019-12-30 17:48:26 +08:00
4b43bd2b9e update 2019-12-30 17:43:56 +08:00
ff6aeb92ec update 2019-12-30 16:57:21 +08:00
a9abdcd9d3 update 2019-12-30 16:48:59 +08:00
7965a020b6 update 2019-12-30 16:37:47 +08:00
17e626493a update 2019-12-30 16:26:25 +08:00
0de055a36b update 2019-12-30 16:25:32 +08:00
5070c851ed update 2019-12-30 16:21:50 +08:00
157a97b35d update 2019-12-30 15:13:11 +08:00
8e6ee35efb update 2019-12-29 13:33:42 +08:00
ec284241ee update 2019-12-29 13:29:52 +08:00
a3f59aac0a update 2019-12-29 13:21:36 +08:00
35c71be4a9 update 2019-12-29 12:12:33 +08:00
abf6dbf73c update 2019-12-29 01:07:38 +08:00
037ecc0a2a update 2019-12-28 17:46:47 +08:00
5a87d59d30 update 2019-12-28 17:34:08 +08:00
1351ec583e update 2019-12-28 17:31:56 +08:00
5b29257227 update 2019-12-28 12:31:47 +08:00
551073cb83 update 2019-12-28 11:41:47 +08:00
a1b466e2c2 update 2019-12-28 11:40:00 +08:00
0b3cf4a2a4 update 2019-12-27 18:01:19 +08:00
0dccfd9f09 update 2019-12-27 17:57:07 +08:00
988f088a58 update 2019-12-27 17:36:02 +08:00
270 changed files with 23266 additions and 7290 deletions

View File

@ -7,16 +7,16 @@ APP_URL=http://localhost
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=db
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=123456
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
@ -31,6 +31,8 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME=null
MAILGUN_DOMAIN=
MAILGUN_SECRET=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
@ -43,4 +45,4 @@ PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

5
.gitignore vendored
View File

@ -1,5 +1,5 @@
/node_modules
/config/v2panel.php
/config/v2board.php
/public/hot
/public/storage
/public/env.example.js
@ -9,6 +9,7 @@
.env.backup
.phpunit.result.cache
.idea
.lock
Homestead.json
Homestead.yaml
npm-debug.log
@ -16,4 +17,4 @@ yarn-error.log
composer.phar
composer.lock
yarn.lock
docker-compose.yml
docker-compose.yml

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2017-2019 Bruskyii Panda
Copyright (c) 2019 Tokumeikoi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -38,6 +38,24 @@ class CheckCommission extends Command
* @return mixed
*/
public function handle()
{
$this->autoCheck();
$this->autoPayCommission();
}
public function autoCheck()
{
if ((int)config('v2board.commission_auto_check_enable', 1)) {
Order::where('commission_status', 0)
->where('status', 3)
->where('updated_at', '<=', strtotime('-3 day', time()))
->update([
'commission_status' => 1
]);
}
}
public function autoPayCommission()
{
$order = Order::where('commission_status', 1)
->where('status', 3)
@ -54,5 +72,5 @@ class CheckCommission extends Command
}
}
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
class CheckExpire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:expire';
/**
* The console command description.
*
* @var string
*/
protected $description = '过期检查';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::all();
foreach ($user as $item) {
if ($item->expired_at < time() || $item->u + $item->d >= $item->transfer_enable) {
$item->enable = 0;
} else {
$item->enable = 1;
}
$item->save();
}
}
}

View File

@ -2,11 +2,14 @@
namespace App\Console\Commands;
use App\Services\OrderService;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use App\Utils\Helper;
use App\Models\Coupon;
use Illuminate\Support\Facades\DB;
class CheckOrder extends Command
{
@ -41,51 +44,115 @@ class CheckOrder extends Command
*/
public function handle()
{
$order = Order::get();
foreach ($order as $item) {
$orders = Order::get();
foreach ($orders as $item) {
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$item->status = 2;
$item->save();
$orderService = new OrderService($item);
$orderService->cancel();
}
break;
case 1:
$this->orderHandle($item);
break;
}
}
}
private function orderHandle ($order) {
private function orderHandle(Order $order)
{
$user = User::find($order->user_id);
return $this->buy($order, $user);
}
private function buy ($order, $user) {
$plan = Plan::find($order->plan_id);
if ($order->refund_amount) {
$user->balance = $user->balance + $order->refund_amount;
}
DB::beginTransaction();
if ($order->surplus_order_ids) {
try {
Order::whereIn('id', json_decode($order->surplus_order_ids))->update([
'status' => 4
]);
} catch (\Exception $e) {
DB::rollback();
abort(500, '开通失败');
}
}
switch ((string)$order->cycle) {
case 'onetime_price':
$this->buyByOneTime($order, $user, $plan);
break;
case 'reset_price':
$this->buyReset($user);
break;
default:
$this->buyByCycle($order, $user, $plan);
}
if (!$user->save()) {
DB::rollBack();
abort(500, '开通失败');
}
$order->status = 3;
if (!$order->save()) {
DB::rollBack();
abort(500, '开通失败');
}
DB::commit();
}
private function buyReset(User $user)
{
$user->u = 0;
$user->d = 0;
}
private function buyByCycle(Order $order, User $user, Plan $plan)
{
// change plan process
if ((int)$order->type === 3) {
$user->expired_at = time();
}
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->enable = 1;
// 续费重置&类型=续费
if ((int)config('v2board.renew_reset_traffic_enable', 1) && $order->type === 2) $this->buyReset($user);
// 购买前用户过期为NULL一次性
if ($user->expired_at === NULL) $this->buyReset($user);
// 新购
if ($order->type === 1) $this->buyReset($user);
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = $this->getTime($order->cycle, $user->expired_at);
if ($user->save()) {
$order->status = 3;
$order->save();
}
}
private function getTime ($str, $timestamp) {
private function buyByOneTime(Order $order, User $user, Plan $plan)
{
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->u = 0;
$user->d = 0;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = NULL;
}
private function getTime($str, $timestamp)
{
if ($timestamp < time()) {
$timestamp = time();
}
switch ($str) {
case 'month_price': return strtotime('+1 month', $timestamp);
case 'quarter_price': return strtotime('+3 month', $timestamp);
case 'half_year_price': return strtotime('+6 month', $timestamp);
case 'year_price': return strtotime('+12 month', $timestamp);
case 'month_price':
return strtotime('+1 month', $timestamp);
case 'quarter_price':
return strtotime('+3 month', $timestamp);
case 'half_year_price':
return strtotime('+6 month', $timestamp);
case 'year_price':
return strtotime('+12 month', $timestamp);
}
}
}

View File

@ -3,10 +3,11 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Models\User;
class ResetTraffic extends Command
{
protected $user;
/**
* The name and signature of the console command.
*
@ -29,6 +30,8 @@ class ResetTraffic extends Command
public function __construct()
{
parent::__construct();
$this->user = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
}
/**
@ -38,7 +41,47 @@ class ResetTraffic extends Command
*/
public function handle()
{
DB::table('v2_user')->update([
$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;
}
}
private function resetByMonthFirstDay($user):void
{
$user = $this->user;
if ((string)date('d') === '01') {
$user->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay():void
{
$user = $this->user;
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($user->get() as $item) {
$expireDay = date('d', $item->expired_at);
$today = date('d');
if ($expireDay === $today) {
array_push($users, $item->id);
}
if (($today === $lastDay) && $expireDay >= $lastDay) {
array_push($users, $item->id);
}
}
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);

View File

@ -0,0 +1,91 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
use App\Jobs\SendEmailJob;
class SendRemindMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:remindMail';
/**
* The console command description.
*
* @var string
*/
protected $description = '发送提醒邮件';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$users = User::all();
foreach ($users as $user) {
if ($user->remind_expire) $this->remindExpire($user);
if ($user->remind_traffic) $this->remindTraffic($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')
]
]);
}
}
private function remindTraffic($user)
{
if ($this->remindTrafficIsWarnValue(($user->u + $user->d), $user->transfer_enable)) {
$sendCount = MailLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('template_name', 'like', '%remindTraffic%')
->count();
if ($sendCount > 0) return;
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的流量使用已达到80%',
'template_name' => 'remindTraffic',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
}
}
private function remindTrafficIsWarnValue($ud, $transfer_enable)
{
if ($ud <= 0) return false;
if (($ud / $transfer_enable * 100) < 80) return false;
return true;
}
}

View File

@ -1,68 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\Order;
use App\Models\Server;
use App\Models\ServerLog;
use App\Utils\Helper;
use Illuminate\Support\Facades\Redis;
class SystemCache extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'system:cache';
/**
* The console command description.
*
* @var string
*/
protected $description = '系统缓存任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->setMonthIncome();
$this->setMonthRegisterTotal();
}
private function setMonthIncome() {
Redis::set(
'month_income',
Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->where('status', '3')
->sum('total_amount')
);
}
private function setMonthRegisterTotal() {
Redis::set(
'month_register_total',
User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count()
);
}
}

View File

@ -3,24 +3,22 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Utils\Helper;
class ImportReset extends Command
class Test extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:reset';
protected $signature = 'test';
/**
* The console command description.
*
* @var string
*/
protected $description = '为导入用户重置所有uuid及token';
protected $description = '';
/**
* Create a new command instance.
@ -39,11 +37,5 @@ class ImportReset extends Command
*/
public function handle()
{
$user = User::all();
foreach ($user as $item) {
$item->v2ray_uuid = Helper::guid(true);
$item->token = Helper::guid();
$item->save();
}
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands;
use App\Utils\CacheKey;
use Illuminate\Console\Command;
use App\Models\ServerLog;
use App\Models\ServerStat;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class V2boardCache extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'v2board:cache';
/**
* The console command description.
*
* @var string
*/
protected $description = '缓存任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
}
private function cacheServerStat()
{
$serverLogs = ServerLog::select(
'server_id',
DB::raw("sum(u) as u"),
DB::raw("sum(d) as d"),
DB::raw("count(*) as online")
)
->where('updated_at', '>=', time() - 3600)
->groupBy('server_id')
->get();
foreach ($serverLogs as $serverLog) {
$data = [
'server_id' => $serverLog->server_id,
'u' => $serverLog->u,
'd' => $serverLog->d,
'online' => $serverLog->online
];
// ServerStat::create($data);
}
}
}

View File

@ -3,6 +3,7 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Support\Facades\DB;
@ -40,51 +41,114 @@ class V2boardInstall extends Command
*/
public function handle()
{
if (\File::exists(base_path() . '/.lock')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.lock文件');
try {
$this->info("__ ______ ____ _ ");
$this->info("\ \ / /___ \| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \ / / __) | _ \ / _ \ / _` | '__/ _` | ");
$this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | ");
$this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| ");
if (\File::exists(base_path() . '/.lock')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.lock文件');
}
if (!\File::exists(base_path() . '/.env')) {
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
}
$this->saveToEnv([
'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')),
'DB_HOST' => $this->ask('请输入数据库地址(默认:localhost', 'localhost'),
'DB_DATABASE' => $this->ask('请输入数据库名'),
'DB_USERNAME' => $this->ask('请输入数据库用户名'),
'DB_PASSWORD' => $this->ask('请输入数据库密码')
]);
\Artisan::call('config:clear');
\Artisan::call('config:cache');
try {
DB::connection()->getPdo();
} catch (\Exception $e) {
abort(500, '数据库连接失败');
}
$file = \File::get(base_path() . '/database/install.sql');
if (!$file) {
abort(500, '数据库文件不存在');
}
$sql = str_replace("\n", "", $file);
$sql = preg_split("/;/", $sql);
if (!is_array($sql)) {
abort(500, '数据库文件格式有误');
}
$this->info('正在导入数据库请稍等...');
foreach ($sql as $item) {
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {
}
}
$this->info('数据库导入完成');
$email = '';
while (!$email) {
$email = $this->ask('请输入管理员邮箱?');
}
$password = '';
while (!$password) {
$password = $this->ask('请输入管理员密码?');
}
if (!$this->registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试');
}
$this->info('一切就绪');
$this->info('访问 http(s)://你的站点/admin 进入管理面板');
\File::put(base_path() . '/.lock', time());
} catch (\Exception $e) {
$this->error($e->getMessage());
}
\Artisan::call('key:generate');
sleep(2);
\Artisan::call('config:cache');
DB::connection()->getPdo();
$file = \File::get(base_path() . '/install.sql');
if (!$file) {
abort(500, '数据库文件不存在');
}
$sql = str_replace("\n", "", $file);
$sql = preg_split("/;/", $sql);
if (!is_array($sql)) {
abort(500, '数据库文件格式有误');
}
$this->info('正在导入数据库请稍等...');
foreach($sql as $item) {
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {}
}
$email = '';
while (!$email) {
$email = $this->ask('请输入管理员邮箱?');
}
$password = '';
while (!$password) {
$password = $this->ask('请输入管理员密码?');
}
if (!$this->registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试');
}
$this->info('一切就绪');
\File::put(base_path() . '/.lock', time());
}
private function registerAdmin ($email, $password) {
private function registerAdmin($email, $password)
{
$user = new User();
$user->email = $email;
if (strlen($password) < 8) {
abort(500, '管理员密码长度最小为8位字符');
}
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->v2ray_uuid = Helper::guid(true);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
$user->is_admin = 1;
return $user->save();
}
private function saveToEnv($data = [])
{
function set_env_var($key, $value)
{
if (! is_bool(strpos($value, ' '))) {
$value = '"' . $value . '"';
}
$key = strtoupper($key);
$envPath = app()->environmentFilePath();
$contents = file_get_contents($envPath);
preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches);
$oldValue = count($matches) ? $matches[0] : '';
if ($oldValue) {
$contents = str_replace("{$oldValue}", "{$key}={$value}", $contents);
} else {
$contents = $contents . "\n{$key}={$value}\n";
}
$file = fopen($envPath, 'w');
fwrite($file, $contents);
return fclose($file);
}
foreach($data as $key => $value) {
set_env_var($key, $value);
}
return true;
}
}

View File

@ -39,22 +39,23 @@ class V2boardUpdate extends Command
public function handle()
{
\Artisan::call('config:cache');
DB::connection()->getPdo();
$file = \File::get(base_path() . '/update.sql');
if (!$file) {
abort(500, '数据库文件不存在');
}
$sql = str_replace("\n", "", $file);
$sql = preg_split("/;/", $sql);
if (!is_array($sql)) {
abort(500, '数据库文件格式有误');
DB::connection()->getPdo();
$file = \File::get(base_path() . '/database/update.sql');
if (!$file) {
abort(500, '数据库文件不存在');
}
$sql = str_replace("\n", "", $file);
$sql = preg_split("/;/", $sql);
if (!is_array($sql)) {
abort(500, '数据库文件格式有误');
}
$this->info('正在导入数据库请稍等...');
foreach($sql as $item) {
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {}
}
foreach ($sql as $item) {
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {
}
}
$this->info('更新完毕');
}
}

View File

@ -19,22 +19,21 @@ class Kernel extends ConsoleKernel
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// check order
// v2board
$schedule->command('v2board:cache')->hourly();
// check
$schedule->command('check:order')->everyMinute();
// check expire
$schedule->command('check:expire')->everyMinute();
// check commission
$schedule->command('check:commission')->everyMinute();
// system cache
$schedule->command('system:cache')->hourly();
// reset
$schedule->command('reset:traffic')->monthlyOn(1, '00:00');
$schedule->command('reset:serverLog')->monthlyOn(1, '00:00');
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->quarterly();
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
}
/**
@ -44,7 +43,7 @@ class Kernel extends ConsoleKernel
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}

View File

@ -29,7 +29,7 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* @param \Exception $exception
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
@ -40,12 +40,15 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if($exception instanceof \Illuminate\Http\Exceptions\ThrottleRequestsException) {
abort(429, '请求频繁,请稍后再试');
}
return parent::render($request, $exception);
}

View File

@ -3,35 +3,69 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ConfigSave;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Order;
use App\Models\User;
class ConfigController extends Controller
{
public function init () {
public function getEmailTemplate()
{
$path = resource_path('views/mail/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function fetch () {
public function setTelegramWebhook(Request $request)
{
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$telegramService->getMe();
$telegramService->setWebhook(
url(
'/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token')))
)
);
return response([
'data' => true
]);
}
public function fetch()
{
// TODO: default should be in Dict
return response([
'data' => [
'invite' => [
'invite_force' => (int)config('v2board.invite_force', 0),
'invite_commission' => config('v2board.invite_commission', 10),
'invite_gen_limit' => config('v2board.invite_gen_limit', 5),
'invite_never_expire' => config('v2board.invite_never_expire', 0)
'invite_never_expire' => config('v2board.invite_never_expire', 0),
'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1),
'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1)
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
'stop_register' => (int)config('v2board.stop_register', 0),
'email_verify' => (int)config('v2board.email_verify', 0),
'app_name' => config('v2board.app_name', 'V2Board'),
'app_description' => config('v2board.app_description', 'V2Board is best!'),
'app_url' => config('v2board.app_url'),
'subscribe_url' => config('v2board.subscribe_url'),
'plan_update_fee' => config('v2board.plan_update_fee', 0.5),
'plan_is_update' => (int)config('v2board.plan_is_update', 1)
'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
'try_out_hour' => (int)config('v2board.try_out_hour', 1),
'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0)
],
'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
'reset_traffic_method' => (int)config('v2board.reset_traffic_method', 0),
'renew_reset_traffic_enable' => (int)config('v2board.renew_reset_traffic_enable', 1)
],
'pay' => [
// alipay
@ -40,38 +74,81 @@ class ConfigController extends Controller
'alipay_pubkey' => config('v2board.alipay_pubkey'),
'alipay_privkey' => config('v2board.alipay_privkey'),
// stripe
'stripe_alipay_enable' => (int)config('v2board.stripe_alipay_enable', 0),
'stripe_wepay_enable' => (int)config('v2board.stripe_wepay_enable', 0),
'stripe_card_enable' => (int)config('v2board.stripe_card_enable', 0),
'stripe_sk_live' => config('v2board.stripe_sk_live'),
'stripe_pk_live' => config('v2board.stripe_pk_live'),
'stripe_alipay_enable' => (int)config('v2board.stripe_alipay_enable'),
'stripe_wepay_enable' => (int)config('v2board.stripe_wepay_enable'),
'stripe_webhook_key' => config('v2board.stripe_webhook_key'),
'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
// bitpayx
'bitpayx_enable' => config('v2board.bitpayx_enable'),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret')
'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// mGate
'mgate_name' => config('v2board.mgate_name', '在线支付'),
'mgate_enable' => (int)config('v2board.mgate_enable', 0),
'mgate_url' => config('v2board.mgate_url'),
'mgate_app_id' => config('v2board.mgate_app_id'),
'mgate_app_secret' => config('v2board.mgate_app_secret'),
// Epay
'epay_name' => config('v2board.epay_name', '在线支付'),
'epay_enable' => (int)config('v2board.epay_enable', 0),
'epay_url' => config('v2board.epay_url'),
'epay_pid' => config('v2board.epay_pid'),
'epay_key' => config('v2board.epay_key'),
],
'frontend' => [
'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
'frontend_background_url' => config('v2board.frontend_background_url'),
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin')
],
'server' => [
'server_token' => config('v2board.server_token')
'server_token' => config('v2board.server_token'),
'server_license' => config('v2board.server_license'),
'server_log_enable' => config('v2board.server_log_enable', 0),
'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'),
'email_port' => config('v2board.email_port'),
'email_username' => config('v2board.email_username'),
'email_password' => config('v2board.email_password'),
'email_encryption' => config('v2board.email_encryption'),
'email_from_address' => config('v2board.email_from_address')
],
'telegram' => [
'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
'telegram_bot_token' => config('v2board.telegram_bot_token')
]
]
]);
}
public function save (ConfigSave $request) {
public function save(ConfigSave $request)
{
$data = $request->input();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, ConfigSave::filter())) {
abort(500, '禁止修改');
if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改');
}
$array[$k] = $v;
}
$data = var_export($array, 1);
if(!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
opcache_reset();
}
\Artisan::call('config:cache');
return response([
'data' => true

View File

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\CouponSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Coupon;
use App\Utils\Helper;
class CouponController extends Controller
{
public function fetch(Request $request)
{
$coupons = Coupon::all();
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
]);
}
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 drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
abort(500, '优惠券不存在');
}
if (!$coupon->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\MailSend;
use App\Services\UserService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Jobs\SendEmailJob;
class MailController extends Controller
{
public function send(MailSend $request)
{
$userService = new UserService();
$users = [];
switch ($request->input('type')) {
case 1: $users = $userService->getAllUsers();
break;
case 2: $users = $userService->getUsersByIds($request->input('receiver'));
break;
// available users
case 3: $users = $userService->getAvailableUsers();
break;
// un available users
case 4: $users = $userService->getUnAvailbaleUsers();
break;
}
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
}
return response([
'data' => true
]);
}
}

View File

@ -6,45 +6,42 @@ use App\Http\Requests\Admin\NoticeSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
return response([
'data' => Notice::orderBy('id', 'DESC')->get()
]);
}
public function save (NoticeSave $request) {
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!Notice::create($data)) {
abort(500, '保存失败');
if (!$request->input('id')) {
if (!Notice::create($data)) {
abort(500, '保存失败');
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function update (NoticeSave $request) {
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!Notice::where('id', $request->input('id'))->update($data)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop (Request $request) {
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}

View File

@ -2,16 +2,21 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Services\OrderService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$orderModel = Order::orderBy('created_at', 'DESC');
@ -21,10 +26,14 @@ class OrderController extends Controller
if ($request->input('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->where('status', 3);
$orderModel->where('commission_balance', '>', 0);
}
if ($request->input('id')) {
$orderModel->where('id', $request->input('id'));
}
if ($request->input('user_id')) {
$orderModel->where('user_id', $request->input('user_id'));
}
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->get();
@ -42,8 +51,9 @@ class OrderController extends Controller
]);
}
public function update (OrderUpdate $request) {
$updateData = $request->only([
public function update(OrderUpdate $request)
{
$params = $request->only([
'status',
'commission_status'
]);
@ -54,7 +64,19 @@ class OrderController extends Controller
abort(500, '订单不存在');
}
if (!$order->update($updateData)) {
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) {
abort(500, '更新失败');
}
@ -63,7 +85,8 @@ class OrderController extends Controller
]);
}
public function repair (Request $request) {
public function repair(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数错误');
}
@ -81,4 +104,50 @@ class OrderController extends Controller
'data' => true
]);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该用户不存在');
}
if (!$plan) {
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->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->cycle === 'reset_price') {
$order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = 3;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2;
} else {
$order->type = 1;
}
$orderService->setInvite($user);
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
}

View File

@ -3,48 +3,78 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
$counts = User::select(
DB::raw("plan_id"),
DB::raw("count(*) as count")
)
->where('plan_id', '!=', NULL)
->where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
})
->groupBy("plan_id")
->get();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
foreach ($counts as $kk => $vv) {
if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count;
}
}
return response([
'data' => Plan::get()
'data' => $plans
]);
}
public function save (PlanSave $request) {
public function save(PlanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
} else {
$plan = new Plan();
DB::beginTransaction();
// update user group id and transfer
try {
User::where('plan_id', $plan->id)->update([
'group_id' => $plan->group_id,
'transfer_enable' => $plan->transfer_enable * 1073741824
]);
$plan->update($params);
} catch (\Exception $e) {
DB::rollBack();
abort(500, '保存失败');
}
DB::commit();
return response([
'data' => true
]);
}
$plan->name = $request->input('name');
$plan->content = $request->input('content');
if ($plan->content) {
$plan->content = str_replace(PHP_EOL, '', $plan->content);
if (!Plan::create($params)) {
abort(500, '创建失败');
}
$plan->transfer_enable = $request->input('transfer_enable');
$plan->group_id = $request->input('group_id');
$plan->month_price = $request->input('month_price');
$plan->quarter_price = $request->input('quarter_price');
$plan->half_year_price = $request->input('half_year_price');
$plan->year_price = $request->input('year_price');
return response([
'data' => $plan->save()
'data' => true
]);
}
public function drop (Request $request) {
public function drop(Request $request)
{
if (Order::where('plan_id', $request->input('id'))->first()) {
abort(500, '该订阅下存在订单无法删除');
}
@ -62,17 +92,21 @@ class PlanController extends Controller
]);
}
public function update (PlanUpdate $request) {
public function update(PlanUpdate $request)
{
$updateData = $request->only([
'show',
'renew'
]);
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
if (!$plan->update($updateData)) {
try {
$plan->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
}
@ -80,4 +114,19 @@ class PlanController extends Controller
'data' => true
]);
}
public function sort(PlanSort $request)
{
DB::beginTransaction();
foreach ($request->input('plan_ids') as $k => $v) {
if (!Plan::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class GroupController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('group_id')) {
return response([
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
return response([
'data' => ServerGroup::get()
]);
}
public function save(Request $request)
{
if (empty($request->input('name'))) {
abort(500, '组名不能为空');
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return response([
'data' => $serverGroup->save()
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
if (!$serverGroup) {
abort(500, '组不存在');
}
}
$servers = Server::all();
foreach ($servers as $server) {
$groupId = json_decode($server->group_id);
if (in_array($request->input('id'), $groupId)) {
abort(500, '该组已被节点所使用,无法删除');
}
}
if (Plan::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被订阅所使用,无法删除');
}
if (User::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被用户所使用,无法删除');
}
return response([
'data' => $serverGroup->delete()
]);
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanSort;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Services\ServerService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerTrojan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class TrojanController extends Controller
{
public function fetch(Request $request)
{
$server = ServerTrojan::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['parent_id']));
} else {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['id']));
}
}
return response([
'data' => $server
]);
}
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) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerTrojan::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerTrojanUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerTrojan::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerTrojan::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
public function sort(ServerTrojanSort $request)
{
DB::beginTransaction();
foreach ($request->input('server_ids') as $k => $v) {
if (!ServerTrojan::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getTrojanConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerV2raySave;
use App\Http\Requests\Admin\ServerV2raySort;
use App\Http\Requests\Admin\ServerV2rayUpdate;
use App\Services\ServerService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Server;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class V2rayController extends Controller
{
public function fetch(Request $request)
{
$server = Server::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server[$i]['parent_id']));
} else {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server[$i]['id']));
}
}
return response([
'data' => $server
]);
}
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'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!Server::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerV2rayUpdate $request)
{
$params = $request->only([
'show',
]);
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!Server::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getVmessConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
public function sort(ServerV2raySort $request)
{
DB::beginTransaction();
foreach ($request->input('server_ids') as $k => $v) {
if (!Server::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -1,144 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ServerSave;
use App\Http\Requests\Admin\ServerUpdate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerGroup;
use App\Models\Server;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Support\Facades\Redis;
class ServerController extends Controller
{
public function fetch (Request $request) {
$server = Server::get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['last_check_at'] = Redis::get('server_last_check_at_' . $server[$i]['id']);
}
return response([
'data' => $server
]);
}
public function save (ServerSave $request) {
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
} else {
$server = new Server();
}
$server->group_id = json_encode($request->input('group_id'));
$server->name = $request->input('name');
$server->host = $request->input('host');
$server->port = $request->input('port');
$server->server_port = $request->input('server_port');
$server->tls = $request->input('tls');
$server->tags = $request->input('tags') ? json_encode($request->input('tags')) : NULL;
$server->rate = $request->input('rate');
$server->network = $request->input('network');
if ($request->input('settings')) {
if (!is_object(json_decode($request->input('settings')))) {
abort(500, '传输协议配置格式不正确');
}
$server->settings = $request->input('settings');
}
return response([
'data' => $server->save()
]);
}
public function groupFetch (Request $request) {
if ($request->input('group_id')) {
return response([
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
return response([
'data' => ServerGroup::get()
]);
}
public function groupSave (Request $request) {
if (empty($request->input('name'))) {
abort(500, '组名不能为空');
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return response([
'data' => $serverGroup->save()
]);
}
public function groupDrop (Request $request) {
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
if (!$serverGroup) {
abort(500, '组不存在');
}
}
$servers = Server::all();
foreach ($servers as $server) {
$groupId = json_decode($server->group_id);
if (in_array($request->input('id'), $groupId)) {
abort(500, '该组已被节点所使用,无法删除');
}
}
if (Plan::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被订阅所使用,无法删除');
}
if (User::where('group_id', $request->input('id'))->first()) {
abort(500, '该组已被用户所使用,无法删除');
}
return response([
'data' => $serverGroup->delete()
]);
}
public function drop (Request $request) {
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update (ServerUpdate $request) {
$updateData = $request->only([
'show',
]);
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
if (!$server->update($updateData)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
}

View File

@ -10,22 +10,36 @@ use App\Models\Plan;
use App\Models\User;
use App\Models\Ticket;
use App\Models\Order;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
class StatController extends Controller
{
public function getOverride (Request $request) {
public function getOverride(Request $request)
{
return response([
'data' => [
'month_income' => Redis::get('month_income'),
'month_register_total' => Redis::get('month_register_total'),
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereIn('status', [3, 4])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pendding_total' => Ticket::where('status', 0)
->count(),
'commission_pendding_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->where('status', 3)
->sum('total_amount'),
'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->where('status', 3)
->sum('total_amount')
]
]);
}

View File

@ -2,15 +2,20 @@
namespace App\Http\Controllers\Admin;
use App\Jobs\SendEmailJob;
use App\Services\TicketService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\User;
use App\Models\TicketMessage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
@ -29,53 +34,49 @@ class TicketController extends Controller
'data' => $ticket
]);
}
$ticket = Ticket::orderBy('created_at', 'DESC')
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('created_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
for ($i = 0; $i < count($ticket); $i++) {
if ($ticket[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$ticket[$i]['reply_status'] = 0;
for ($i = 0; $i < count($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$ticket[$i]['reply_status'] = 1;
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $ticket
'data' => $res,
'total' => $total
]);
}
public function reply (Request $request) {
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
if ($ticket->status) {
abort(500, '工单已关闭,无法回复');
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
$ticket->last_reply_user_id = $request->session()->get('id');
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
}
DB::commit();
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->session()->get('id')
);
return response([
'data' => true
]);
}
public function close (Request $request) {
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\TutorialSave;
use App\Http\Requests\Admin\TutorialSort;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Tutorial;
use Illuminate\Support\Facades\DB;
class TutorialController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Tutorial::orderBy('sort', 'ASC')->get()
]);
}
public function save(TutorialSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Tutorial::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Tutorial::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$tutorial = Tutorial::find($request->input('id'));
if (!$tutorial) {
abort(500, '教程不存在');
}
$tutorial->show = $tutorial->show ? 0 : 1;
if (!$tutorial->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(TutorialSort $request)
{
DB::beginTransaction();
foreach ($request->input('tutorial_ids') as $k => $v) {
if (!Tutorial::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$tutorial = Tutorial::find($request->input('id'));
if (!$tutorial) {
abort(500, '教程不存在');
}
if (!$tutorial->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -11,13 +11,19 @@ use App\Models\Plan;
class UserController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$userModel = User::orderBy('created_at', 'DESC');
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$userModel = User::orderBy($sort, $sortType);
if ($request->input('email')) {
$userModel->where('email', $request->input('email'));
}
if ($request->input('invite_user_id')) {
$userModel->where('invite_user_id', $request->input('invite_user_id'));
}
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->get();
@ -35,53 +41,43 @@ class UserController extends Controller
]);
}
public function id2UserInfo ($id) {
if (empty($id)) {
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
return response([
'data' => User::select([
'email',
'u',
'd',
'transfer_enable',
'expired_at'
])->find($id)
'data' => User::find($request->input('id'))
]);
}
public function update (UserUpdate $request) {
$updateData = $request->only([
'email',
'password',
'transfer_enable',
'expired_at',
'banned',
'plan_id',
'commission_rate',
'is_admin'
]);
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
}
if (User::where('email', $updateData['email'])->first() && $user->email !== $updateData['email']) {
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
abort(500, '邮箱已被使用');
}
if (isset($updateData['password'])) {
$updateData['password'] = password_hash($updateData['password'], PASSWORD_DEFAULT);
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($updateData['password']);
unset($params['password']);
}
$updateData['transfer_enable'] = $updateData['transfer_enable'] * 1073741824;
if (isset($updateData['plan_id'])) {
$plan = Plan::find($updateData['plan_id']);
if (isset($params['plan_id'])) {
$plan = Plan::find($params['plan_id']);
if (!$plan) {
abort(500, '订阅计划不存在');
}
$updateData['group_id'] = $plan->group_id;
$params['group_id'] = $plan->group_id;
}
if (!$user->update($updateData)) {
try {
$user->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([

View File

@ -1,102 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\Notice;
use App\Utils\Helper;
class AppController extends Controller
{
CONST CLIENT_CONFIG = '{"policy":{"levels":{"0":{"uplinkOnly":0}}},"dns":{"servers":["114.114.114.114","8.8.8.8"]},"outboundDetour":[{"protocol":"freedom","tag":"direct","settings":{}}],"inbound":{"listen":"0.0.0.0","port":31211,"protocol":"socks","settings":{"auth":"noauth","udp":true,"ip":"127.0.0.1"}},"inboundDetour":[{"listen":"0.0.0.0","allocate":{"strategy":"always","refresh":5,"concurrency":3},"port":31210,"protocol":"http","tag":"httpDetour","domainOverride":["http","tls"],"streamSettings":{},"settings":{"timeout":0}}],"routing":{"strategy":"rules","settings":{"domainStrategy":"IPIfNonMatch","rules":[{"port":"1-52","type":"field","outboundTag":"direct"},{"port":"54-79","type":"field","outboundTag":"direct"},{"port":"81-442","type":"field","outboundTag":"direct"},{"port":"444-65535","type":"field","outboundTag":"direct"},{"type":"field","ip":["0.0.0.0/8","10.0.0.0/8","100.64.0.0/10","127.0.0.0/8","169.254.0.0/16","172.16.0.0/12","192.0.0.0/24","192.0.2.0/24","192.168.0.0/16","198.18.0.0/15","198.51.100.0/24","203.0.113.0/24","::1/128","fc00::/7","fe80::/10"],"outboundTag":"direct"},{"type":"field","ip":["geoip:cn"],"outboundTag":"direct"}]}},"outbound":{"tag":"proxy","sendThrough":"0.0.0.0","mux":{"enabled":false,"concurrency":8},"protocol":"vmess","settings":{"vnext":[{"address":"server","port":443,"users":[{"id":"uuid","alterId":2,"security":"auto","level":0}],"remark":"remark"}]},"streamSettings":{"network":"tcp","tcpSettings":{"header":{"type":"none"}},"security":"none","tlsSettings":{"allowInsecure":true,"allowInsecureCiphers":true},"kcpSettings":{"header":{"type":"none"},"mtu":1350,"congestion":false,"tti":20,"uplinkCapacity":5,"writeBufferSize":1,"readBufferSize":1,"downlinkCapacity":20},"wsSettings":{"path":"","headers":{"Host":"server.cc"}}}}}';
CONST SOCKS_PORT = 10010;
CONST HTTP_PORT = 10011;
public function data (Request $request) {
$user = $request->user;
$nodes = [];
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, '订阅计划不存在');
}
if ($user->expired_at > time()) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
foreach ($servers as $item) {
$groupId = json_decode($item['group_id']);
if (in_array($user->group_id, $groupId)) {
array_push($nodes, $item);
}
}
}
}
return response([
'data' => [
'nodes' => $nodes,
'u' => $user->u,
'd' => $user->d,
'transfer_enable' => $user->transfer_enable,
'expired_at' => $user->expired_at,
'plan' => isset($user['plan']) ? $user['plan'] : false,
'notice' => Notice::orderBy('created_at', 'DESC')->first()
]
]);
}
public function config (Request $request) {
if (empty($request->input('server_id'))) {
abort(500, '参数错误');
}
$user = $request->user;
if ($user->expired_at < time()) {
abort(500, '订阅计划已过期');
}
$server = Server::where('show', 1)
->where('id', $request->input('server_id'))
->first();
if (!$server) {
abort(500, '服务器不存在');
}
$json = json_decode(self::CLIENT_CONFIG);
//socks
$json->inbound->port = (int)self::SOCKS_PORT;
//http
$json->inboundDetour[0]->port = (int)self::HTTP_PORT;
//other
$json->outbound->settings->vnext[0]->address = (string)$server->host;
$json->outbound->settings->vnext[0]->port = (int)$server->port;
$json->outbound->settings->vnext[0]->users[0]->id = (string)$user->v2ray_uuid;
$json->outbound->settings->vnext[0]->users[0]->alterId = (int)$user->v2ray_alter_id;
$json->outbound->settings->vnext[0]->remark = (string)$server->name;
$json->outbound->streamSettings->network = $server->network;
if ($server->settings) {
switch ($server->network) {
case 'tcp': $json->outbound->streamSettings->tcpSettings = json_decode($server->settings);
break;
case 'kcp': $json->outbound->streamSettings->kcpSettings = json_decode($server->settings);
break;
case 'ws': $json->outbound->streamSettings->wsSettings = json_decode($server->settings);
break;
case 'http': $json->outbound->streamSettings->httpSettings = json_decode($server->settings);
break;
case 'domainsocket': $json->outbound->streamSettings->dsSettings = json_decode($server->settings);
break;
case 'quic': $json->outbound->streamSettings->quicSettings = json_decode($server->settings);
break;
}
}
if ($request->input('is_global')) {
$json->routing->settings->rules[5]->outboundTag = 'proxy';
}
if ($server->tls) {
$json->outbound->streamSettings->security = "tls";
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Clash;
use Illuminate\Http\Request;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
CONST CLIENT_CONFIG = '{"policy":{"levels":{"0":{"uplinkOnly":0}}},"dns":{"servers":["114.114.114.114","8.8.8.8"]},"outboundDetour":[{"protocol":"freedom","tag":"direct","settings":{}}],"inbound":{"listen":"0.0.0.0","port":31211,"protocol":"socks","settings":{"auth":"noauth","udp":true,"ip":"127.0.0.1"}},"inboundDetour":[{"listen":"0.0.0.0","allocate":{"strategy":"always","refresh":5,"concurrency":3},"port":31210,"protocol":"http","tag":"httpDetour","domainOverride":["http","tls"],"streamSettings":{},"settings":{"timeout":0}}],"routing":{"strategy":"rules","settings":{"domainStrategy":"IPIfNonMatch","rules":[{"type":"field","ip":["geoip:cn"],"outboundTag":"direct"},{"type":"field","ip":["0.0.0.0/8","10.0.0.0/8","100.64.0.0/10","127.0.0.0/8","169.254.0.0/16","172.16.0.0/12","192.0.0.0/24","192.0.2.0/24","192.168.0.0/16","198.18.0.0/15","198.51.100.0/24","203.0.113.0/24","::1/128","fc00::/7","fe80::/10"],"outboundTag":"direct"}]}},"outbound":{"tag":"proxy","sendThrough":"0.0.0.0","mux":{"enabled":false,"concurrency":8},"protocol":"vmess","settings":{"vnext":[{"address":"server","port":443,"users":[{"id":"uuid","alterId":2,"security":"auto","level":0}],"remark":"remark"}]},"streamSettings":{"network":"tcp","tcpSettings":{"header":{"type":"none"}},"security":"none","tlsSettings":{"allowInsecure":true,"allowInsecureCiphers":true},"kcpSettings":{"header":{"type":"none"},"mtu":1350,"congestion":false,"tti":20,"uplinkCapacity":5,"writeBufferSize":1,"readBufferSize":1,"downlinkCapacity":20},"wsSettings":{"path":"","headers":{"Host":"server.cc"}}}}}';
CONST SOCKS_PORT = 10010;
CONST HTTP_PORT = 10011;
public function getConfig(Request $request)
{
$server = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
}
$config = Yaml::parseFile(base_path() . '/resources/rules/app.clash.yaml');
$proxy = [];
$proxies = [];
foreach ($servers['vmess'] as $item) {
array_push($proxy, Clash::buildVmess($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($servers['trojan'] as $item) {
array_push($proxy, Clash::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) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
die(Yaml::dump($config));
}
public function getVersion()
{
return response([
'data' => [
'version' => '4.0.0',
'download_url' => ''
]
]);
}
public function config(Request $request)
{
if (empty($request->input('server_id'))) {
abort(500, '参数错误');
}
$user = $request->user;
if ($user->expired_at < time() && $user->expired_at !== NULL) {
abort(500, '订阅计划已过期');
}
$server = Server::where('show', 1)
->where('id', $request->input('server_id'))
->first();
if (!$server) {
abort(500, '服务器不存在');
}
$json = json_decode(self::CLIENT_CONFIG);
//socks
$json->inbound->port = (int)self::SOCKS_PORT;
//http
$json->inboundDetour[0]->port = (int)self::HTTP_PORT;
//other
$json->outbound->settings->vnext[0]->address = (string)$server->host;
$json->outbound->settings->vnext[0]->port = (int)$server->port;
$json->outbound->settings->vnext[0]->users[0]->id = (string)$user->uuid;
$json->outbound->settings->vnext[0]->users[0]->alterId = (int)$user->v2ray_alter_id;
$json->outbound->settings->vnext[0]->remark = (string)$server->name;
$json->outbound->streamSettings->network = $server->network;
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->outbound->streamSettings->tcpSettings = json_decode($server->networkSettings);
break;
case 'kcp':
$json->outbound->streamSettings->kcpSettings = json_decode($server->networkSettings);
break;
case 'ws':
$json->outbound->streamSettings->wsSettings = json_decode($server->networkSettings);
break;
case 'http':
$json->outbound->streamSettings->httpSettings = json_decode($server->networkSettings);
break;
case 'domainsocket':
$json->outbound->streamSettings->dsSettings = json_decode($server->networkSettings);
break;
case 'quic':
$json->outbound->streamSettings->quicSettings = json_decode($server->networkSettings);
break;
}
}
if ($request->input('is_global')) {
$json->routing->settings->rules[0]->outboundTag = 'proxy';
}
if ($server->tls) {
$json->outbound->streamSettings->security = "tls";
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@ -0,0 +1,225 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Utils\Clash;
use App\Utils\QuantumultX;
use App\Utils\Shadowrocket;
use App\Utils\Surge;
use Illuminate\Http\Request;
use App\Models\Server;
use App\Utils\Helper;
use Symfony\Component\Yaml\Yaml;
use App\Services\UserService;
class ClientController extends Controller
{
public function subscribe(Request $request)
{
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$_SERVER['HTTP_USER_AGENT'] = strtolower($_SERVER['HTTP_USER_AGENT']);
if (strpos($_SERVER['HTTP_USER_AGENT'], 'quantumult%20x') !== false) {
die($this->quantumultX($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'quantumult') !== false) {
die($this->quantumult($user, $servers['vmess']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'clash') !== false) {
die($this->clash($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'surfboard') !== false) {
die($this->surfboard($user, $servers['vmess']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'surge') !== false) {
die($this->surge($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'shadowrocket') !== false) {
die($this->shadowrocket($user, $servers['vmess'], $servers['trojan']));
}
}
die($this->origin($user, $servers['vmess'], $servers['trojan']));
}
}
// TODO: Ready to stop support
private function quantumult($user, $vmess = [])
{
$uri = '';
header('subscription-userinfo: upload=' . $user->u . '; download=' . $user->d . ';total=' . $user->transfer_enable);
foreach ($vmess as $item) {
$str = '';
$str .= $item->name . '= vmess, ' . $item->host . ', ' . $item->port . ', chacha20-ietf-poly1305, "' . $user->uuid . '", over-tls=' . ($item->tls ? "true" : "false") . ', certificate=0, group=' . config('v2board.app_name', 'V2Board');
if ($item->network === 'ws') {
$str .= ', obfs=ws';
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $str .= ', obfs-path="' . $wsSettings->path . '"';
if (isset($wsSettings->headers->Host)) $str .= ', obfs-header="Host:' . $wsSettings->headers->Host . '"';
}
}
$uri .= "vmess://" . base64_encode($str) . "\r\n";
}
return base64_encode($uri);
}
private function shadowrocket($user, $vmess = [], $trojan = [])
{
$uri = '';
//display remaining traffic and expire date
$upload = round($user->u / (1024*1024*1024), 2);
$download = round($user->d / (1024*1024*1024), 2);
$totalTraffic = round($user->transfer_enable / (1024*1024*1024), 2);
$expiredDate = date('Y-m-d', $user->expired_at);
$uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n";
foreach ($vmess as $item) {
$uri .= Shadowrocket::buildVmess($user->uuid, $item);
}
foreach ($trojan as $item) {
$uri .= Shadowrocket::buildTrojan($user->uuid, $item);
}
return base64_encode($uri);
}
private function quantumultX($user, $vmess = [], $trojan = [])
{
$uri = '';
header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
foreach ($vmess as $item) {
$uri .= QuantumultX::buildVmess($user->uuid, $item);
}
foreach ($trojan as $item) {
$uri .= QuantumultX::buildTrojan($user->uuid, $item);
}
return base64_encode($uri);
}
private function origin($user, $vmess = [], $trojan = [])
{
$uri = '';
foreach ($vmess as $item) {
$uri .= Helper::buildVmessLink($item, $user);
}
foreach ($trojan as $item) {
$uri .= Helper::buildTrojanLink($item, $user);
}
return base64_encode($uri);
}
private function surge($user, $vmess = [], $trojan = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($vmess as $item) {
// [Proxy]
$proxies .= Surge::buildVmess($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
foreach ($trojan as $item) {
// [Proxy]
$proxies .= Surge::buildTrojan($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
$defaultConfig = base_path() . '/resources/rules/default.surge.conf';
$customConfig = base_path() . '/resources/rules/custom.surge.conf';
if (\File::exists($customConfig)) {
$config = file_get_contents("$customConfig");
} else {
$config = file_get_contents("$defaultConfig");
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
private function surfboard($user, $vmess = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($vmess as $item) {
// [Proxy]
$proxies .= $item->name . ' = vmess, ' . $item->host . ', ' . $item->port . ', username=' . $user->uuid;
if ($item->tls) {
$tlsSettings = json_decode($item->tlsSettings);
$proxies .= ', tls=' . ($item->tls ? "true" : "false");
if (isset($tlsSettings->allowInsecure)) {
$proxies .= ', skip-cert-verify=' . ($tlsSettings->allowInsecure ? "true" : "false");
}
}
if ($item->network == 'ws') {
$proxies .= ', ws=true';
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $proxies .= ', ws-path=' . $wsSettings->path;
if (isset($wsSettings->headers->Host)) $proxies .= ', ws-headers=host:' . $wsSettings->headers->Host;
}
}
$proxies .= "\r\n";
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
$defaultConfig = base_path() . '/resources/rules/default.surfboard.conf';
$customConfig = base_path() . '/resources/rules/custom.surfboard.conf';
if (\File::exists($customConfig)) {
$config = file_get_contents("$customConfig");
} else {
$config = file_get_contents("$defaultConfig");
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
private function clash($user, $vmess = [], $trojan = [])
{
$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 ($vmess as $item) {
array_push($proxy, Clash::buildVmess($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($trojan as $item) {
array_push($proxy, Clash::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) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
}

View File

@ -1,143 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Utils\Helper;
use Symfony\Component\Yaml\Yaml;
class ClientController extends Controller
{
public function subscribe (Request $request) {
$user = $request->user;
$server = [];
if ($user->expired_at > time()) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
foreach ($servers as $item) {
$groupId = json_decode($item['group_id']);
if (in_array($user->group_id, $groupId)) {
array_push($server, $item);
}
}
}
if(isset($_SERVER['HTTP_USER_AGENT'])) {
if(strpos($_SERVER['HTTP_USER_AGENT'], 'Quantumult%20X') !== false) {
die($this->quantumultX($user, $server));
}
if(strpos($_SERVER['HTTP_USER_AGENT'], 'Quantumult') !== false) {
die($this->quantumult($user, $server));
}
if(strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'clash') !== false) {
die($this->clash($user, $server));
}
}
die($this->origin($user, $server));
}
private function quantumultX ($user, $server) {
$uri = '';
foreach($server as $item) {
$uri .= "vmess=".$item->host.":".$item->port.", method=none, password=".$user->v2ray_uuid.", fast-open=false, udp-relay=false, tag=".$item->name;
if ($item->network == 'ws') {
$uri .= ', obfs=ws';
if ($item->settings) {
$wsSettings = json_decode($item->settings);
if ($wsSettings->path) $uri .= ', obfs-uri='.$wsSettings->path;
}
}
$uri .= "\r\n";
}
return base64_encode($uri);
}
private function quantumult ($user, $server) {
$uri = '';
header('subscription-userinfo: upload='.$user->u.'; download='.$user->d.';total='.$user->transfer_enable);
foreach($server as $item) {
$str = '';
$str .= $item->name.'= vmess, '.$item->host.', '.$item->port.', chacha20-ietf-poly1305, "'.$user->v2ray_uuid.'", over-tls='.($item->tls?"true":"false").', certificate=0, group='.config('v2board.app_name', 'V2Board');
if ($item->network === 'ws') {
$str .= ', obfs=ws';
if ($item->settings) {
$wsSettings = json_decode($item->settings);
if ($wsSettings->path) $str .= ', obfs-path="'.$wsSettings->path.'"';
if ($wsSettings->headers->Host) $str .= ', obfs-header="Host:'.$wsSettings->headers->Host.'"';
}
}
$uri .= "vmess://".base64_encode($str)."\r\n";
}
return base64_encode($uri);
}
private function origin ($user, $server) {
$uri = '';
foreach($server as $item) {
$uri .= Helper::buildVmessLink($item, $user);
}
return base64_encode($uri);
}
private function clash ($user, $server) {
$proxy = [];
$proxyGroup = [];
$proxies = [];
foreach ($server as $item) {
$array = [];
$array['name'] = $item->name;
$array['type'] = 'vmess';
$array['server'] = $item->host;
$array['port'] = $item->port;
$array['uuid'] = $user->v2ray_uuid;
$array['alterId'] = $user->v2ray_alter_id;
$array['cipher'] = 'auto';
if ($item->tls) {
$array['tls'] = true;
}
if ($item->network == 'ws') {
$array['network'] = $item->network;
if ($item->settings) {
$wsSettings = json_decode($item->settings);
if ($wsSettings->path) $array['ws-path'] = $wsSettings->path;
if ($wsSettings->headers->Host) $array['ws-headers'] = [
'Host' => $wsSettings->headers->Host
];
}
}
array_push($proxy, $array);
array_push($proxies, $item->name);
}
array_push($proxyGroup, [
'name' => config('v2board.app_name', 'V2Board'),
'type' => 'select',
'proxies' => $proxies
]);
$config = [
'port' => 7890,
'socks-port' => 0,
'allow-lan' => false,
'mode' => 'Rule',
'log-level' => 'info',
'external-controller' => '0.0.0.0:9090',
'secret' => '',
'Proxy' => $proxy,
'Proxy Group' => $proxyGroup,
'Rule' => [
'DOMAIN-SUFFIX,google.com,'.config('v2board.app_name', 'V2Board'),
'DOMAIN-KEYWORD,google,'.config('v2board.app_name', 'V2Board'),
'DOMAIN,google.com,'.config('v2board.app_name', 'V2Board'),
'DOMAIN-SUFFIX,ad.com,REJECT',
'IP-CIDR,127.0.0.0/8,DIRECT',
'GEOIP,CN,DIRECT',
'MATCH,'.config('v2board.app_name', 'V2Board')
]
];
return Yaml::dump($config);
}
}

View File

@ -2,18 +2,23 @@
namespace App\Http\Controllers\Guest;
use App\Services\OrderService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Library\Epay;
use Omnipay\Omnipay;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
use Library\BitpayX;
use Library\MGate;
class OrderController extends Controller
{
public function alipayNotify (Request $request) {
Log::info('alipayNotifyData: ' . json_encode($_POST));
public function alipayNotify(Request $request)
{
// Log::info('alipayNotifyData: ' . json_encode($_POST));
$gateway = Omnipay::create('Alipay_AopF2F');
$gateway->setSignType('RSA2'); //RSA/RSA2
$gateway->setAppId(config('v2board.alipay_appid'));
@ -24,8 +29,8 @@ class OrderController extends Controller
try {
/** @var \Omnipay\Alipay\Responses\AopCompletePurchaseResponse $response */
$response = $request->send();
if($response->isPaid()){
if ($response->isPaid()) {
/**
* Payment is successful
*/
@ -34,7 +39,7 @@ class OrderController extends Controller
}
die('success'); //The response should be 'success' only
}else{
} else {
/**
* Payment is not successful
*/
@ -48,8 +53,9 @@ class OrderController extends Controller
}
}
public function stripeNotify (Request $request) {
Log::info('stripeNotifyData: ' . json_encode($request->input()));
public function stripeNotify(Request $request)
{
// Log::info('stripeNotifyData: ' . json_encode($request->input()));
\Stripe\Stripe::setApiKey(config('v2board.stripe_sk_live'));
try {
@ -63,21 +69,26 @@ class OrderController extends Controller
}
switch ($event->type) {
case 'source.chargeable':
$source = $event->data->object;
$charge = \Stripe\Charge::create([
'amount' => $source['amount'],
'currency' => $source['currency'],
'source' => $source['id'],
$object = $event->data->object;
\Stripe\Charge::create([
'amount' => $object->amount,
'currency' => $object->currency,
'source' => $object->id,
'metadata' => json_decode($object->metadata, true)
]);
if ($charge['status'] == 'succeeded') {
$trade_no = Redis::get($source['id']);
if (!$trade_no) {
abort(500, 'redis is not found trade no by stripe source id');
die('success');
break;
case 'charge.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
$metaData = isset($object->metadata->out_trade_no) ? $object->metadata : $object->source->metadata;
$tradeNo = $metaData->out_trade_no;
if (!$tradeNo) {
abort(500, 'trade no is not found in metadata');
}
if (!$this->handle($trade_no, $source['id'])) {
if (!$this->handle($tradeNo, $object->balance_transaction)) {
abort(500, 'fail');
}
Redis::del($source['id']);
die('success');
}
break;
@ -86,57 +97,80 @@ class OrderController extends Controller
}
}
public function bitpayXNotify (Request $request) {
public function bitpayXNotify(Request $request)
{
$inputString = file_get_contents('php://input', 'r');
Log::info('bitpayXNotifyData: ' . $inputString);
// Log::info('bitpayXNotifyData: ' . $inputString);
$inputStripped = str_replace(array("\r", "\n", "\t", "\v"), '', $inputString);
$inputJSON = json_decode($inputStripped, true); //convert JSON into array
$bitpayX = new BitpayX(config('v2board.bitpayx_appsecret'));
$params = [
'status' => $inputJSON['status'],
'order_id' => $inputJSON['order_id'],
'merchant_order_id' => $inputJSON['merchant_order_id'],
'price_amount' => $inputJSON['price_amount'],
'price_currency' => $inputJSON['price_currency'],
'pay_amount' => $inputJSON['pay_amount'],
'pay_currency' => $inputJSON['pay_currency'],
'created_at_t' => $inputJSON['created_at_t']
'status' => $inputJSON['status'],
'order_id' => $inputJSON['order_id'],
'merchant_order_id' => $inputJSON['merchant_order_id'],
'price_amount' => $inputJSON['price_amount'],
'price_currency' => $inputJSON['price_currency'],
'pay_amount' => $inputJSON['pay_amount'],
'pay_currency' => $inputJSON['pay_currency'],
'created_at_t' => $inputJSON['created_at_t']
];
$strToSign = $bitpayX->prepareSignId($inputJSON['merchant_order_id']);
if (!$bitpayX->verify($strToSign, $inputJSON['token'])) {
die([
'status' => 400,
'error' => 'sign error'
]);
abort(500, 'sign error');
}
if ($params['status'] !== 'PAID') {
die([
'status' => 400,
'error' => 'order is not paid'
]);
abort(500, 'order is not paid');
}
if (!$this->handle($params['merchant_order_id'], $params['order_id'])) {
die([
'status' => 400,
'error' => 'order process fail'
]);
abort(500, 'order process fail');
}
die([
die(json_encode([
'status' => 200
]);
]));
}
private function handle ($tradeNo, $callbackNo) {
public function mgateNotify(Request $request)
{
$mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
if (!$mgate->verify($request->input())) {
abort(500, 'fail');
}
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
abort(500, 'fail');
}
die('success');
}
public function epayNotify(Request $request)
{
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
if (!$epay->verify($request->input())) {
abort(500, 'fail');
}
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
abort(500, 'fail');
}
die('success');
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if (!$order) {
abort(500, 'order is not found');
}
if ($order->status !== 0) {
abort(500, 'order is paid');
$orderService = new OrderService($order);
if (!$orderService->success($callbackNo)) {
return false;
}
$order->status = 1;
$order->callback_no = $callbackNo;
return $order->save();
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}

View File

@ -8,7 +8,8 @@ use App\Models\Plan;
class PlanController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return response([
'data' => $plan

View File

@ -0,0 +1,197 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Utils\Helper;
use App\Services\TicketService;
class TelegramController extends Controller
{
protected $msg;
public function __construct(Request $request)
{
if ($request->input('access_token') !== md5(config('v2board.telegram_bot_token'))) {
abort(500, 'authentication failed');
}
}
public function webhook(Request $request)
{
$this->msg = $this->getMessage($request->input());
if (!$this->msg) return;
try {
switch($this->msg->message_type) {
case 'send':
$this->fromSend();
break;
case 'reply':
$this->fromReply();
break;
}
} catch (\Exception $e) {
$telegramService = new TelegramService();
$telegramService->sendMessage($this->msg->chat_id, $e->getMessage());
}
}
private function fromSend()
{
switch($this->msg->command) {
case '/bind': $this->bind();
break;
case '/traffic': $this->traffic();
break;
case '/getlatesturl': $this->getLatestUrl();
break;
case '/unbind': $this->unbind();
break;
default: $this->help();
}
}
private function fromReply()
{
// ticket
if (preg_match("/[#](.*)/", $this->msg->reply_text, $match)) {
$this->replayTicket($match[1]);
}
}
private function getMessage(array $data)
{
if (!isset($data['message'])) return false;
$obj = new \StdClass();
$obj->is_private = $data['message']['chat']['type'] === 'private' ? true : false;
if (!isset($data['message']['text'])) return false;
$text = explode(' ', $data['message']['text']);
$obj->command = $text[0];
$obj->args = array_slice($text, 1);
$obj->chat_id = $data['message']['chat']['id'];
$obj->message_id = $data['message']['message_id'];
$obj->message_type = !isset($data['message']['reply_to_message']) ? 'send' : 'reply';
$obj->text = $data['message']['text'];
if ($obj->message_type === 'reply') {
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
return $obj;
}
private function bind()
{
$msg = $this->msg;
if (!$msg->is_private) return;
if (!isset($msg->args[0])) {
abort(500, '参数有误,请携带订阅地址发送');
}
$subscribeUrl = $msg->args[0];
$subscribeUrl = parse_url($subscribeUrl);
parse_str($subscribeUrl['query'], $query);
$token = $query['token'];
if (!$token) {
abort(500, '订阅地址无效');
}
$user = User::where('token', $token)->first();
if (!$user) {
abort(500, '用户不存在');
}
if ($user->telegram_id) {
abort(500, '该账号已经绑定了Telegram账号');
}
$user->telegram_id = $msg->chat_id;
if (!$user->save()) {
abort(500, '设置失败');
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, '绑定成功');
}
private function unbind()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
if (!$user) {
$this->help();
$telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
}
$user->telegram_id = NULL;
if (!$user->save()) {
abort(500, '解绑失败');
}
$telegramService->sendMessage($msg->chat_id, '解绑成功', 'markdown');
}
private function help()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$telegramService = new TelegramService();
$commands = [
'/bind 订阅地址 - 绑定你的' . config('v2board.app_name', 'V2Board') . '账号',
'/traffic - 查询流量信息',
'/getlatesturl - 获取最新的' . config('v2board.app_name', 'V2Board') . '网址',
'/unbind - 解除绑定'
];
$text = implode(PHP_EOL, $commands);
$telegramService->sendMessage($msg->chat_id, "你可以使用以下命令进行操作:\n\n$text", 'markdown');
}
private function traffic()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
if (!$user) {
$this->help();
$telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
}
$transferEnable = Helper::trafficConvert($user->transfer_enable);
$up = Helper::trafficConvert($user->u);
$down = Helper::trafficConvert($user->d);
$remaining = Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d));
$text = "🚥流量查询\n———————————————\n计划流量:`{$transferEnable}`\n已用上行:`{$up}`\n已用下行:`{$down}`\n剩余流量:`{$remaining}`";
$telegramService->sendMessage($msg->chat_id, $text, 'markdown');
}
private function getLatestUrl()
{
$msg = $this->msg;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
$text = sprintf(
"%s的最新网址是%s",
config('v2board.app_name', 'V2Board'),
config('v2board.app_url')
);
$telegramService->sendMessage($msg->chat_id, $text, 'markdown');
}
private function replayTicket($ticketId)
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
if (!$user) {
abort(500, '用户不存在');
}
$ticketService = new TicketService();
if ($user->is_admin) {
$ticketService->replyByAdmin(
$ticketId,
$msg->text,
$user->id
);
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use App\Utils\Helper;
class NoticeController extends Controller
{
public function fetch (Request $request) {
return response([
'data' => Notice::orderBy('created_at', 'DESC')->first()
]);
}
}

View File

@ -1,303 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\OrderSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Utils\Helper;
use Omnipay\Omnipay;
use Stripe\Stripe;
use Stripe\Source;
use Library\BitpayX;
class OrderController extends Controller
{
public function fetch (Request $request) {
$order = Order::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC')
->get();
$plan = Plan::get();
for($i = 0; $i < count($order); $i++) {
for($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return response([
'data' => $order
]);
}
public function details (Request $request) {
$order = Order::where('user_id', $request->session()->get('id'))
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
$order['plan'] = Plan::find($order->plan_id);
$order['update_fee'] = config('v2board.plan_update_fee', 0.5);
if (!$order['plan']) {
abort(500, '订阅不存在');
}
return response([
'data' => $order
]);
}
public function save (OrderSave $request) {
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
if (!($plan->show || $user->plan_id == $plan->id)) {
abort(500, '该订阅已售罄');
}
if (!$plan->show && !$plan->renew) {
abort(500, '该订阅无法续费,请更换其他订阅');
}
if (!(int)$plan[$request->input('cycle')]) {
abort(500, '该订阅周期无法进行购买,请选择其他周期');
}
$order = new 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')];
if ($user->expired_at > time() && $order->plan_id !== $user->plan_id) {
$order->type = 3;
if (!(int)config('v2board.plan_is_update', 1)) abort(500, '目前不允许更改订阅,请联系管理员');
$order->total_amount = $order->total_amount + (ceil(($user->expired_at - time()) / 86400) * config('v2board.plan_update_fee', 0.5) * 100);
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2;
} else {
$order->type = 1;
}
if ($user->invite_user_id) {
$order->invite_user_id = $user->invite_user_id;
$inviter = User::find($user->invite_user_id);
if ($inviter && $inviter->commission_rate) {
$order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100);
} else {
$order->commission_balance = $order->total_amount * (config('v2board.invite_commission', 10) / 100);
}
}
if (!$order->save()) {
abort(500, '订单创建失败');
}
return response([
'data' => $order->trade_no
]);
}
public function checkout (Request $request) {
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或已支付');
}
switch ($method) {
// return type => 0: QRCode / 1: URL
case 0:
// alipayF2F
if (!(int)config('v2board.alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->alipayF2F($tradeNo, $order->total_amount)
]);
case 2:
// stripeAlipay
if (!(int)config('v2board.stripe_alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->stripeAlipay($order)
]);
case 3:
// stripeWepay
if (!(int)config('v2board.stripe_wepay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->stripeWepay($order)
]);
case 4:
// bitpayX
if (!(int)config('v2board.bitpayx_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->bitpayX($order)
]);
default:
abort(500, '支付方式不存在');
}
}
public function check (Request $request) {
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
return response([
'data' => $order->status
]);
}
public function getPaymentMethod () {
$data = [];
if ((int)config('v2board.alipay_enable')) {
$alipayF2F = new \StdClass();
$alipayF2F->name = '支付宝';
$alipayF2F->method = 0;
$alipayF2F->icon = 'alipay';
array_push($data, $alipayF2F);
}
if ((int)config('v2board.stripe_alipay_enable')) {
$stripeAlipay = new \StdClass();
$stripeAlipay->name = '支付宝';
$stripeAlipay->method = 2;
$stripeAlipay->icon = 'alipay';
array_push($data, $stripeAlipay);
}
if ((int)config('v2board.stripe_wepay_enable')) {
$stripeWepay = new \StdClass();
$stripeWepay->name = '微信';
$stripeWepay->method = 3;
$stripeWepay->icon = 'wechat';
array_push($data, $stripeWepay);
}
if ((int)config('v2board.bitpayx_enable')) {
$bitpayX = new \StdClass();
$bitpayX->name = '虚拟货币';
$bitpayX->method = 4;
$bitpayX->icon = 'bitcoin';
array_push($data, $bitpayX);
}
return response([
'data' => $data
]);
}
private function alipayF2F ($tradeNo, $totalAmount) {
$gateway = Omnipay::create('Alipay_AopF2F');
$gateway->setSignType('RSA2'); //RSA/RSA2
$gateway->setAppId(config('v2board.alipay_appid'));
$gateway->setPrivateKey(config('v2board.alipay_privkey')); // 可以是路径,也可以是密钥内容
$gateway->setAlipayPublicKey(config('v2board.alipay_pubkey')); // 可以是路径,也可以是密钥内容
$gateway->setNotifyUrl(url('/api/v1/guest/order/alipayNotify'));
$request = $gateway->purchase();
$request->setBizContent([
'subject' => config('v2board.app_name', 'V2Board') . ' - 订阅',
'out_trade_no' => $tradeNo,
'total_amount' => $totalAmount / 100
]);
/** @var \Omnipay\Alipay\Responses\AopTradePreCreateResponse $response */
$response = $request->send();
$result = $response->getAlipayResponse();
if ($result['code'] !== '10000') {
abort(500, $result['sub_msg']);
}
// 获取收款二维码内容
return $response->getQrCode();
}
private function stripeAlipay ($order) {
$exchange = Helper::exchange('CNY', 'HKD');
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => 'hkd',
'type' => 'alipay',
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['redirect']['url']) {
abort(500, '支付网关请求失败');
}
if (!Redis::set($source['id'], $order->trade_no)) {
abort(500, '订单创建失败');
}
Redis::expire($source['id'], 3600);
return $source['redirect']['url'];
}
private function stripeWepay ($order) {
$exchange = Helper::exchange('CNY', 'HKD');
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => 'hkd',
'type' => 'wechat',
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['wechat']['qr_code_url']) {
abort(500, '支付网关请求失败');
}
if (!Redis::set($source['id'], $order->trade_no)) {
abort(500, '订单创建失败');
}
Redis::expire($source['id'], 3600);
return $source['wechat']['qr_code_url'];
}
private function bitpayX ($order) {
$bitpayX = new BitpayX(config('v2board.bitpayx_appsecret'));
$params = [
'merchant_order_id' => 'V2Board_' . $order->trade_no,
'price_amount' => $order->total_amount / 100,
'price_currency' => 'CNY',
'title' => '支付单号:' . $order->trade_no,
'description' => '充值:' . $order->total_amount / 100 . ' 元',
'callback_url' => url('/api/v1/guest/order/bitpayXNotify'),
'success_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'cancel_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
];
$strToSign = $bitpayX->prepareSignId($params['merchant_order_id']);
$params['token'] = $bitpayX->sign($strToSign);
$result = $bitpayX->mprequest($params);
Log::info('bitpayXSubmit: ' . json_encode($result));
return isset($result['payment_url']) ? $result['payment_url'] : false;
}
}

View File

@ -0,0 +1,225 @@
<?php
namespace App\Http\Controllers\Passport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthRegister;
use App\Http\Requests\Passport\AuthForget;
use App\Http\Requests\Passport\AuthLogin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Plan;
use App\Models\User;
use App\Models\InviteCode;
use App\Utils\Helper;
use App\Utils\Dict;
use App\Utils\CacheKey;
class AuthController extends Controller
{
public function register(AuthRegister $request)
{
if ((int)config('v2board.email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
abort(500, '邮箱后缀不处于白名单中');
}
}
if ((int)config('v2board.email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
abort(500, '不支持Gmail别名邮箱');
}
}
if ((int)config('v2board.stop_register', 0)) {
abort(500, '本站已关闭注册');
}
if ((int)config('v2board.invite_force', 0)) {
if (empty($request->input('invite_code'))) {
abort(500, '必须使用邀请码才可以注册');
}
}
if ((int)config('v2board.email_verify', 0)) {
if (empty($request->input('email_code'))) {
abort(500, '邮箱验证码不能为空');
}
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
abort(500, '邮箱已存在系统中');
}
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
->where('status', 0)
->first();
if (!$inviteCode) {
if ((int)config('v2board.invite_force', 0)) {
abort(500, '邀请码无效');
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)config('v2board.invite_never_expire', 0)) {
$inviteCode->status = 1;
$inviteCode->save();
}
}
}
// try out
if ((int)config('v2board.try_out_plan_id', 0)) {
$plan = Plan::find(config('v2board.try_out_plan_id'));
if ($plan) {
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (config('v2board.try_out_hour', 1) * 3600);
}
}
if (!$user->save()) {
abort(500, '注册失败');
}
if ((int)config('v2board.email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
return response()->json([
'data' => true
]);
}
public function login(AuthLogin $request)
{
$email = $request->input('email');
$password = $request->input('password');
$user = User::where('email', $email)->first();
if (!$user) {
abort(500, '用户名或密码错误');
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$password,
$user->password)
) {
abort(500, '用户名或密码错误');
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
}
$data = [
'token' => $user->token
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
$data['is_admin'] = true;
}
return response([
'data' => $data
]);
}
// 准备废弃
public function token2Login(Request $request)
{
if ($request->input('token')) {
$redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$location = config('v2board.app_url') . $redirect;
} else {
$location = url($redirect);
}
return header('Location:' . $location);
}
if ($request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key);
if (!$userId) {
abort(500, '令牌有误');
}
$user = User::find($userId);
if (!$user) {
abort(500, '用户不存在');
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
}
Cache::forget($key);
return response([
'data' => true
]);
}
}
public function getTempToken(Request $request)
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, '用户不存在');
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
return response([
'data' => $code
]);
}
public function check(Request $request)
{
$data = [
'is_login' => $request->session()->get('id') ? true : false
];
if ($request->session()->get('is_admin')) {
$data['is_admin'] = true;
}
return response([
'data' => $data
]);
}
public function forget(AuthForget $request)
{
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
}
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该邮箱不存在系统中');
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
if (!$user->save()) {
abort(500, '重置失败');
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return response([
'data' => true
]);
}
}

View File

@ -7,52 +7,81 @@ use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Redis;
use App\Utils\Helper;
use Illuminate\Support\Facades\Cache;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\Dict;
use App\Utils\CacheKey;
class CommController extends Controller
{
public function config () {
public function config()
{
return response([
'data' => [
'isEmailVerify' => (int)config('v2board.email_verify', 0) ? 1 : 0,
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'emailWhitelistSuffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0
]
]);
}
private function isEmailVerify () {
private function isEmailVerify()
{
return response([
'data' => (int)config('v2board.email_verify', 0) ? 1 : 0
]);
}
public function sendEmailVerify (CommSendEmailVerify $request) {
public function sendEmailVerify(CommSendEmailVerify $request)
{
$email = $request->input('email');
$redisKey = 'sendEmailVerify:' . $email;
if (Redis::get($redisKey)) {
abort(500, '验证码已发送,请过一会在请求');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
abort(500, '验证码已发送,请过一会再请求');
}
$code = rand(100000, 999999);
$subject = config('v2board.app_name', 'V2Board') . '邮箱验证码';
Mail::send(
'mail.sendEmailVerify',
[
'code' => $code,
'name' => config('v2board.app_name', 'V2Board')
],
function ($message) use($email, $subject) {
$message->to($email)->subject($subject);
}
);
if (count(Mail::failures()) >= 1) {
// 发送失败
abort(500, '发送失败');
}
Redis::set($redisKey, $code);
Redis::expire($redisKey, 600);
SendEmailJob::dispatch([
'email' => $email,
'subject' => $subject,
'template_name' => 'verify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'code' => $code,
'url' => config('v2board.app_url')
]
]);
Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $email), $code, 300);
Cache::put(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email), time(), 60);
return response([
'data' => true
]);
}
public function pv(Request $request)
{
$inviteCode = InviteCode::where('code', $request->input('invite_code'))->first();
if ($inviteCode) {
$inviteCode->pv = $inviteCode->pv + 1;
$inviteCode->save();
}
return response([
'data' => true
]);
}
private function getEmailSuffix()
{
$suffix = config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Passport;
use App\Http\Requests\Passport\ForgetIndex;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Redis;
class ForgetController extends Controller
{
public function index (ForgetIndex $request) {
$redisKey = 'sendEmailVerify:' . $request->input('email');
if (Redis::get($redisKey) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
}
$user = User::where('email', $request->input('email'))->first();
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
if (!$user->save()) {
abort(500, '重置失败');
}
Redis::del($redisKey);
return response([
'data' => true
]);
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace App\Http\Controllers\Passport;
use Illuminate\Http\Request;
use App\Http\Requests\Passport\LoginIndex;
use App\Http\Controllers\Controller;
use App\Models\User;
class LoginController extends Controller
{
public function index (LoginIndex $request) {
$email = $request->input('email');
$password = $request->input('password');
$user = User::where('email', $email)->first();
if (!$user) {
abort(500, '用户名或密码错误');
}
if (!password_verify($password, $user->password)) {
abort(500, '用户名或密码错误');
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
}
return response([
'data' => [
'is_admin' => $user->is_admin ? 2 : 1,
'token' => $user->token
]
]);
}
public function token2Login (Request $request) {
if (empty($request->input('token'))) {
abort(500, '参数错误');
}
$redirect = $request->input('redirect') ? $request->input('redirect') : 'dashboard';
$user = User::where('token', $request->input('token'))->first();
if ($user) {
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
}
}
if (config('v2board.app_url')) {
$location = config('v2board.app_url') . '/#/' . $redirect;
} else {
$location = url('/#/' . $redirect);
}
header('Location:' . $location);
}
public function check (Request $request) {
return response([
'data' => $request->session()->get('id') ? true : false
]);
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers\Passport;
use App\Http\Requests\Passport\RegisterIndex;
use App\Http\Requests\Passport\RegisterSendEmailVerify;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Redis;
use App\Utils\Helper;
use App\Models\InviteCode;
class RegisterController extends Controller
{
public function index (RegisterIndex $request) {
if ((int)config('v2board.stop_register', 0)) {
abort(500, '本站已关闭注册');
}
if ((int)config('v2board.invite_force', 0)) {
if (empty($request->input('invite_code'))) {
abort(500, '必须使用邀请码才可以注册');
}
}
if ((int)config('v2board.email_verify', 0)) {
$redisKey = 'sendEmailVerify:' . $request->input('email');
if (empty($request->input('email_code'))) {
abort(500, '邮箱验证码不能为空');
}
if (Redis::get($redisKey) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
abort(500, '邮箱已存在系统中');
}
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->v2ray_uuid = Helper::guid(true);
$user->token = Helper::guid();
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
->where('status', 0)
->first();
if (!$inviteCode) {
if ((int)config('v2board.invite_force', 0)) {
abort(500, '邀请码无效');
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)config('v2board.invite_never_expire', env('V2BOARD_INVITE_NEVER_EXPIRE'))) {
$inviteCode->status = 1;
$inviteCode->save();
}
}
}
if (!$user->save()) {
abort(500, '注册失败');
}
if ((int)config('v2board.email_verify', 0)) {
Redis::del($redisKey);
}
return response()->json([
'data' => true
]);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
class PlanController extends Controller
{
public function fetch (Request $request) {
if (empty($request->input('plan_id'))) {
abort(500, '参数错误');
}
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
return response([
'data' => $plan
]);
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers\Server;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller as BaseController;
class Controller extends BaseController
{
public function __construct(Request $request) {
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
}
}

View File

@ -2,49 +2,55 @@
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\Server\Controller;
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\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
class DeepbworkController extends Controller
{
CONST SERVER_CONFIG = '{"api":{"services":["HandlerService","StatsService"],"tag":"api"},"stats":{},"inbound":{"port":443,"protocol":"vmess","settings":{"clients":[]},"streamSettings":{"network":"tcp"},"tag":"proxy"},"inboundDetour":[{"listen":"0.0.0.0","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"outbound":{"protocol":"freedom","settings":{}},"routing":{"settings":{"rules":[{"inboundTag":["api"],"outboundTag":"api","type":"field"}]},"strategy":"rules"},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user (Request $request) {
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Redis::set('server_last_check_at_' . $server->id, time());
$users = User::whereIn('group_id', json_decode($server->group_id))
->select([
'id',
'email',
't',
'u',
'd',
'transfer_enable',
'enable',
'v2ray_uuid',
'v2ray_alter_id',
'v2ray_level'
])
->get();
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->v2ray_uuid,
"email" => sprintf("%s@v2panel.user", $user->v2ray_uuid),
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
];
unset($user['v2ray_uuid']);
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
@ -56,78 +62,62 @@ class DeepbworkController extends Controller
}
// 后端提交数据
public function submit (Request $request) {
Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 1,
'msg' => 'ok'
]);
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$user = User::find($item['user_id']);
$user->t = time();
$user->u = $user->u + $u;
$user->d = $user->d + $d;
$user->save();
$serverLog = new ServerLog();
$serverLog->user_id = $item['user_id'];
$serverLog->server_id = $request->input('node_id');
$serverLog->u = $item['u'];
$serverLog->d = $item['d'];
$serverLog->rate = $server->rate;
$serverLog->save();
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'])) {
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'vmess'
);
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config (Request $request) {
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(1000, '参数错误');
abort(500, '参数错误');
}
$server = Server::find($nodeId);
if (!$server) {
abort(1001, '节点不存在');
$serverService = new ServerService();
try {
$json = $serverService->getVmessConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
$json = json_decode(self::SERVER_CONFIG);
$json->inboundDetour[0]->port = (int)$localPort;
$json->inbound->port = (int)$server->server_port;
$json->inbound->streamSettings->network = $server->network;
if ($server->settings) {
switch ($server->network) {
case 'tcp': $json->inbound->streamSettings->tcpSettings = json_decode($server->settings);
break;
case 'kcp': $json->inbound->streamSettings->kcpSettings = json_decode($server->settings);
break;
case 'ws': $json->inbound->streamSettings->wsSettings = json_decode($server->settings);
break;
case 'http': $json->inbound->streamSettings->httpSettings = json_decode($server->settings);
break;
case 'domainsocket': $json->inbound->streamSettings->dsSettings = json_decode($server->settings);
break;
case 'quic': $json->inbound->streamSettings->quicSettings = json_decode($server->settings);
break;
}
}
if ((int)$server->tls) {
$json->inbound->streamSettings->security = "tls";
$tls = (object) array("certificateFile" => "/home/v2ray.crt", "keyFile" => "/home/v2ray.key");
$json->inbound->streamSettings->tlsSettings->certificates[0] = $tls;
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@ -0,0 +1,154 @@
<?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" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
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);
$serverService = new ServerService();
$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'])) {
return $this->error("user fetch fail", 500);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'vmess'
);
}
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->getVmessConfig($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) {
return response([
'msg' => 'ok',
'data' => $data,
]);
}
}

View File

@ -0,0 +1,120 @@
<?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\ServerTrojan;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Trojan
* Github: https://github.com/tokumeikoi/tidalab-trojan
*/
class TrojanTidalabController extends Controller
{
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$result = [];
foreach ($users as $user) {
$user->trojan_user = [
"password" => $user->uuid,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
}
return response([
'msg' => 'ok',
'data' => $result,
]);
}
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerTrojan::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
$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'])) {
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'trojan'
);
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\User;
use App\Utils\Helper;
class ServerController extends Controller {
public function fetch (Request $request) {
$user = User::find($request->session()->get('id'));
$server = [];
if ($user->expired_at > time()) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
foreach ($servers as $item) {
$groupId = json_decode($item['group_id']);
if (in_array($user->group_id, $groupId)) {
array_push($server, $item);
}
}
}
for ($i = 0; $i < count($server); $i++) {
$server[$i]['link'] = Helper::buildVmessLink($server[$i], $user);
$server[$i]['last_check_at'] = Redis::get('server_last_check_at_' . $server[$i]['id']);
}
return response([
'data' => $server
]);
}
public function logFetch (Request $request) {
$type = $request->input('type') ? $request->input('type') : 0;
$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');
switch ($type) {
case 0: $serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
break;
case 1: $serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2: $serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
}
$sum = [
'u' => $serverLogModel->sum('u'),
'd' => $serverLogModel->sum('d')
];
$total = $serverLogModel->count();
$res = $serverLogModel->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total,
'sum' => $sum
]);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
class TutorialController extends Controller
{
public function getSubscribeUrl (Request $request) {
$user = User::find($request->session()->get('id'));
return response([
'data' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token']
]
]);
}
public function getAppleID (Request $request) {
$user = User::find($request->session()->get('id'));
if ($user->expired_at < time()) {
return response([
'data' => [
]
]);
}
return response([
'data' => [
'apple_id' => config('v2board.apple_id'),
'apple_id_password' => config('v2board.apple_id_password')
]
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'isTelegram' => (int)config('v2board.telegram_bot_enable', 0),
'stripePk' => config('v2board.stripe_pk_live')
]
]);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Coupon;
class CouponController extends Controller
{
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, '优惠券码不能为空');
}
$coupon = Coupon::where('code', $request->input('code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
}
if (time() < $coupon->started_at) {
abort(500, '优惠券还未到可用时间');
}
if (time() > $coupon->ended_at) {
abort(500, '优惠券已过期');
}
if ($coupon->limit_plan_ids) {
$limitPlanIds = json_decode($coupon->limit_plan_ids);
info($limitPlanIds);
if (!in_array($request->input('plan_id'), $limitPlanIds)) {
abort(500, '这个计划无法使用该优惠码');
}
}
return response([
'data' => $coupon
]);
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Order;
use App\Models\InviteCode;
@ -11,7 +11,8 @@ use App\Utils\Helper;
class InviteController extends Controller
{
public function save (Request $request) {
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->session()->get('id'))->where('status', 0)->count() >= config('v2board.invite_gen_limit', 5)) {
abort(500, '已达到创建数量上限');
}
@ -23,9 +24,11 @@ class InviteController extends Controller
]);
}
public function details (Request $request) {
public function details(Request $request)
{
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('commission_balance', '>', 0)
->where('status', 3)
->select([
'id',
@ -38,11 +41,12 @@ class InviteController extends Controller
]);
}
public function fetch (Request $request) {
public function fetch(Request $request)
{
$codes = InviteCode::where('user_id', $request->session()->get('id'))
->where('status', 0)
->get();
$commission_rate = config('v2board.invite_commission');
$commission_rate = config('v2board.invite_commission', 10);
$user = User::find($request->session()->get('id'));
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;
@ -52,7 +56,7 @@ class InviteController extends Controller
(int)User::where('invite_user_id', $request->session()->get('id'))->count(),
//有效的佣金
(int)Order::where('status', 3)
->where('commission_status', 1)
->where('commission_status', 2)
->where('invite_user_id', $request->session()->get('id'))
->sum('commission_balance'),
//确认中的佣金
@ -61,7 +65,9 @@ class InviteController extends Controller
->where('invite_user_id', $request->session()->get('id'))
->sum('commission_balance'),
//佣金比例
(int)$commission_rate
(int)$commission_rate,
//可用佣金
(int)$user->commission_balance
];
return response([
'data' => [

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Notice;
use App\Utils\Helper;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC');
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@ -0,0 +1,498 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Utils\Helper;
use Omnipay\Omnipay;
use Stripe\Stripe;
use Stripe\Source;
use Library\BitpayX;
use Library\MGate;
use Library\Epay;
class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return response([
'data' => $order
]);
}
public function details(Request $request)
{
$order = Order::where('user_id', $request->session()->get('id'))
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)config('v2board.try_out_plan_id');
if (!$order['plan']) {
abort(500, '订阅不存在');
}
return response([
'data' => $order
]);
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->session()->get('id'))) {
abort(500, '您有未付款或开通中的订单,请稍后或取消再试');
}
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('cycle') !== 'reset_price') {
abort(500, '该订阅已售罄');
}
}
if (!$plan->renew && $user->plan_id == $plan->id) {
abort(500, '该订阅无法续费,请更换其他订阅');
}
if ($plan[$request->input('cycle')] === NULL) {
if ($request->input('cycle') === 'reset_price') {
abort(500, '该订阅当前不支持重置流量');
}
abort(500, '该订阅周期无法进行购买,请选择其他周期');
}
if ($request->input('cycle') === 'reset_price' && !$user->plan_id) {
abort(500, '必须存在订阅才可以购买流量重置包');
}
if ($request->input('cycle') === 'reset_price' && $user->expired_at <= time()) {
abort(500, '当前订阅已过期,无法购买重置包');
}
DB::beginTransaction();
$order = new Order();
$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')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, '优惠券使用失败');
}
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
DB::rollBack();
abort(500, '余额不足');
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, '余额不足');
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
public function checkout(Request $request)
{
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或已支付');
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
return response([
'type' => -1,
'data' => true
]);
}
switch ($method) {
// return type => 0: QRCode / 1: URL / 2: No action
case 0:
// alipayF2F
if (!(int)config('v2board.alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->alipayF2F($tradeNo, $order->total_amount)
]);
case 2:
// stripeAlipay
if (!(int)config('v2board.stripe_alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->stripeAlipay($order)
]);
case 3:
// stripeWepay
if (!(int)config('v2board.stripe_wepay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->stripeWepay($order)
]);
case 4:
// bitpayX
if (!(int)config('v2board.bitpayx_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->bitpayX($order)
]);
case 5:
if (!(int)config('v2board.mgate_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->mgate($order)
]);
case 6:
if (!(int)config('v2board.stripe_card_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 2,
'data' => $this->stripeCard($order, $request->input('token'))
]);
case 7:
if (!(int)config('v2board.epay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->epay($order)
]);
default:
abort(500, '支付方式不存在');
}
}
public function check(Request $request)
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
return response([
'data' => $order->status
]);
}
public function getPaymentMethod()
{
$data = [];
if ((int)config('v2board.alipay_enable')) {
$alipayF2F = new \StdClass();
$alipayF2F->name = '支付宝';
$alipayF2F->method = 0;
$alipayF2F->icon = 'alipay';
array_push($data, $alipayF2F);
}
if ((int)config('v2board.stripe_alipay_enable')) {
$stripeAlipay = new \StdClass();
$stripeAlipay->name = '支付宝';
$stripeAlipay->method = 2;
$stripeAlipay->icon = 'alipay';
array_push($data, $stripeAlipay);
}
if ((int)config('v2board.stripe_wepay_enable')) {
$stripeWepay = new \StdClass();
$stripeWepay->name = '微信';
$stripeWepay->method = 3;
$stripeWepay->icon = 'wechat';
array_push($data, $stripeWepay);
}
if ((int)config('v2board.bitpayx_enable')) {
$bitpayX = new \StdClass();
$bitpayX->name = config('v2board.bitpayx_name', '在线支付');
$bitpayX->method = 4;
$bitpayX->icon = 'wallet';
array_push($data, $bitpayX);
}
if ((int)config('v2board.mgate_enable')) {
$obj = new \StdClass();
$obj->name = config('v2board.mgate_name', '在线支付');
$obj->method = 5;
$obj->icon = 'wallet';
array_push($data, $obj);
}
if ((int)config('v2board.stripe_card_enable')) {
$obj = new \StdClass();
$obj->name = '信用卡';
$obj->method = 6;
$obj->icon = 'card';
array_push($data, $obj);
}
if ((int)config('v2board.epay_enable')) {
$obj = new \StdClass();
$obj->name = config('v2board.epay_name', '在线支付');
$obj->method = 7;
$obj->icon = 'wallet';
array_push($data, $obj);
}
return response([
'data' => $data
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数有误');
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) {
abort(500, '只可以取消待支付订单');
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '取消失败');
}
return response([
'data' => true
]);
}
private function alipayF2F($tradeNo, $totalAmount)
{
$gateway = Omnipay::create('Alipay_AopF2F');
$gateway->setSignType('RSA2'); //RSA/RSA2
$gateway->setAppId(config('v2board.alipay_appid'));
$gateway->setPrivateKey(config('v2board.alipay_privkey')); // 可以是路径,也可以是密钥内容
$gateway->setAlipayPublicKey(config('v2board.alipay_pubkey')); // 可以是路径,也可以是密钥内容
$gateway->setNotifyUrl(url('/api/v1/guest/order/alipayNotify'));
$request = $gateway->purchase();
$request->setBizContent([
'subject' => config('v2board.app_name', 'V2Board') . ' - 订阅',
'out_trade_no' => $tradeNo,
'total_amount' => $totalAmount / 100
]);
/** @var \Omnipay\Alipay\Responses\AopTradePreCreateResponse $response */
$response = $request->send();
$result = $response->getAlipayResponse();
if ($result['code'] !== '10000') {
abort(500, $result['sub_msg']);
}
// 获取收款二维码内容
return $response->getQrCode();
}
private function stripeAlipay($order)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'alipay',
'statement_descriptor' => $order->trade_no,
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
],
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['redirect']['url']) {
abort(500, '支付网关请求失败');
}
return $source['redirect']['url'];
}
private function stripeWepay($order)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'wechat',
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
],
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['wechat']['qr_code_url']) {
abort(500, '支付网关请求失败');
}
return $source['wechat']['qr_code_url'];
}
private function stripeCard($order, string $token)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
try {
$charge = \Stripe\Charge::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'source' => $token,
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
]
]);
} catch (\Exception $e) {
abort(500, '遇到了点问题,请刷新页面稍后再试');
}
info($charge);
if (!$charge->paid) {
abort(500, '扣款失败,请检查信用卡信息');
}
return $charge->paid;
}
private function bitpayX($order)
{
$bitpayX = new BitpayX(config('v2board.bitpayx_appsecret'));
$params = [
'merchant_order_id' => $order->trade_no,
'price_amount' => $order->total_amount / 100,
'price_currency' => 'CNY',
'title' => '支付单号:' . $order->trade_no,
'description' => '充值:' . $order->total_amount / 100 . ' 元',
'callback_url' => url('/api/v1/guest/order/bitpayXNotify'),
'success_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'cancel_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
];
$strToSign = $bitpayX->prepareSignId($params['merchant_order_id']);
$params['token'] = $bitpayX->sign($strToSign);
$result = $bitpayX->mprequest($params);
// Log::info('bitpayXSubmit: ' . json_encode($result));
return isset($result['payment_url']) ? $result['payment_url'] : false;
}
private function mgate($order)
{
$mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
$result = $mgate->pay([
'app_id' => config('v2board.mgate_app_id'),
'out_trade_no' => $order->trade_no,
'total_amount' => $order->total_amount,
'notify_url' => url('/api/v1/guest/order/mgateNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]);
return $result;
}
private function epay($order)
{
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
return $epay->pay([
'money' => $order->total_amount / 100,
'name' => $order->trade_no,
'notify_url' => url('/api/v1/guest/order/epayNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'out_trade_no' => $order->trade_no
]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Plan;
class PlanController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))
->first();
if (!$plan) {
abort(500, '该订阅不存在');
}
return response([
'data' => $plan
]);
}
$plan = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
return response([
'data' => $plan
]);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\User;
use App\Utils\Helper;
class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->session()->get('id'));
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
$servers = array_merge($servers['vmess'], $servers['trojan']);
}
return response([
'data' => $servers
]);
}
public function logFetch(Request $request)
{
$type = $request->input('type') ? $request->input('type') : 0;
$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');
switch ($type) {
case 0:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
break;
case 1:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
}
$total = $serverLogModel->count();
$res = $serverLogModel->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\TelegramService;
class TelegramController extends Controller
{
public function getBotInfo()
{
$telegramService = new TelegramService();
$response = $telegramService->getMe();
return response([
'data' => [
'username' => $response->result->username
]
]);
}
}

View File

@ -1,18 +1,22 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\User;
use App\Http\Requests\TicketSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\TicketSave;
use App\Http\Requests\User\TicketWithdraw;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Utils\Helper;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function fetch (Request $request) {
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
@ -47,8 +51,12 @@ class TicketController extends Controller
]);
}
public function save (TicketSave $request) {
public function save(TicketSave $request)
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
abort(500, '存在其他工单尚未处理');
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
@ -70,12 +78,14 @@ class TicketController extends Controller
abort(500, '工单创建失败');
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
return response([
'data' => true
]);
}
public function reply (Request $request) {
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
@ -106,13 +116,15 @@ class TicketController extends Controller
abort(500, '工单回复失败');
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
return response([
'data' => true
]);
}
public function close (Request $request) {
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
@ -131,9 +143,53 @@ class TicketController extends Controller
]);
}
private function getLastMessage ($ticketId) {
private function getLastMessage($ticketId)
{
return TicketMessage::where('ticket_id', $ticketId)
->orderBy('id', 'DESC')
->first();
}
public function withdraw(TicketWithdraw $request)
{
DB::beginTransaction();
$subject = '[提现申请]本工单由系统发出';
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->session()->get('id'),
'last_reply_user_id' => $request->session()->get('id')
]);
if (!$ticket) {
DB::rollback();
abort(500, '工单创建失败');
}
$methodText = [
'alipay' => '支付宝',
'paypal' => '贝宝(Paypal)',
'usdt' => 'USDT',
'btc' => '比特币'
];
$message = "提现方式:{$methodText[$request->input('withdraw_method')]}\r\n提现账号:{$request->input('withdraw_account')}\r\n";
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
return response([
'data' => true
]);
}
private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
{
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`");
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Tutorial;
use Illuminate\Support\Facades\DB;
class TutorialController extends Controller
{
public function getSubscribeUrl(Request $request)
{
$user = User::find($request->session()->get('id'));
return response([
'data' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token']
]
]);
}
public function getAppleID(Request $request)
{
$user = User::find($request->session()->get('id'));
if ($user->expired_at < time()) {
return response([
'data' => [
]
]);
}
return response([
'data' => [
'apple_id' => config('v2board.apple_id'),
'apple_id_password' => config('v2board.apple_id_password')
]
]);
}
public function fetch(Request $request)
{
if ($request->input('id')) {
$tutorial = Tutorial::where('show', 1)
->where('id', $request->input('id'))
->first();
if (!$tutorial) {
abort(500, '教程不存在');
}
return response([
'data' => $tutorial
]);
}
$tutorial = Tutorial::select(['id', 'category_id', 'title'])
->where('show', 1)
->orderBy('sort', 'ASC')
->get()
->groupBy('category_id');
$user = User::find($request->session()->get('id'));
$response = [
'data' => [
'tutorials' => $tutorial,
'safe_area_var' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'],
'app_name' => config('v2board.app_name', 'V2board'),
'apple_id' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id', '本站暂无提供AppleID信息') : '账号过期或未订阅',
'apple_id_password' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id_password', '本站暂无提供AppleID信息') : '账号过期或未订阅'
]
]
];
// fuck support shadowrocket urlsafeb64 subscribe
$response['data']['safe_area_var']['b64_subscribe_url'] = str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($response['data']['safe_area_var']['subscribe_url'])
);
// end
// fuck support surge urlencode subscribe
$response['data']['safe_area_var']['ue_subscribe_url'] = urlencode($response['data']['safe_area_var']['subscribe_url']);
// end
return response($response);
}
}

View File

@ -1,10 +1,11 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\User;
use App\Http\Requests\UserUpdate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
@ -15,24 +16,26 @@ use App\Models\ServerLog;
class UserController extends Controller
{
public function logout (Request $request) {
public function logout(Request $request)
{
$request->session()->flush();
return response([
'data' => $request->session()->flush()
'data' => true
]);
}
public function changePassword (Request $request) {
if (empty($request->input('old_password'))) {
abort(500, '旧密码不能为空');
}
if (empty($request->input('new_password'))) {
abort(500, '新密码不能为空');
}
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->session()->get('id'));
if (!password_verify($request->input('old_password'), $user->password)) {
if (!Helper::multiPasswordVerify(
$user->password_algo,
$request->input('old_password'),
$user->password)
) {
abort(500, '旧密码有误');
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
if (!$user->save()) {
abort(500, '保存失败');
}
@ -41,20 +44,26 @@ class UserController extends Controller
'data' => true
]);
}
public function info (Request $request) {
public function info(Request $request)
{
$user = User::where('id', $request->session()->get('id'))
->select([
'email',
'transfer_enable',
'last_login_at',
'created_at',
'enable',
'banned',
'is_admin',
'remind_expire',
'remind_traffic',
'expired_at',
'balance',
'commission_balance'
'commission_balance',
'plan_id',
'discount',
'commission_rate',
'telegram_id'
])
->first();
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
@ -63,7 +72,8 @@ class UserController extends Controller
]);
}
public function getStat (Request $request) {
public function getStat(Request $request)
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->session()->get('id'))
@ -79,7 +89,8 @@ class UserController extends Controller
]);
}
public function getSubscribe (Request $request) {
public function getSubscribe(Request $request)
{
$user = User::find($request->session()->get('id'));
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
@ -88,34 +99,39 @@ class UserController extends Controller
}
}
$user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$user['reset_day'] = $this->getResetDay($user);
return response([
'data' => $user
]);
}
public function resetSecurity (Request $request) {
public function resetSecurity(Request $request)
{
$user = User::find($request->session()->get('id'));
$user->v2ray_uuid = Helper::guid(true);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, '重置失败');
}
return response([
'data' => true
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
]);
}
public function update (UserUpdate $request) {
public function update(UserUpdate $request)
{
$updateData = $request->only([
'remind_expire',
'remind_traffic'
]);
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
}
if (!$user->update($updateData)) {
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
}
@ -123,4 +139,46 @@ class UserController extends Controller
'data' => true
]);
}
public function transfer(Request $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
}
if ($request->input('transfer_amount') <= 0) {
abort(500, '参数错误');
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, '推广佣金余额不足');
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
abort(500, '划转失败');
}
return response([
'data' => true
]);
}
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
if ((int)config('v2board.reset_traffic_method') === 0) {
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1) {
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
return $lastDay - $today + $day;
}
}
return null;
}
}

View File

@ -43,7 +43,7 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\CORS::class,
'throttle:60,1',
'throttle:120,1',
'bindings',
],
];

View File

@ -9,8 +9,8 @@ class Admin
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)

View File

@ -9,12 +9,12 @@ class Authenticate extends Middleware
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
if (!$request->expectsJson()) {
return route('login');
}
}

View File

@ -9,9 +9,9 @@ class CORS
public function handle($request, Closure $next)
{
$origin = $request->header('origin');
if(empty($origin)){
if (empty($origin)) {
$referer = $request->header('referer');
if(!empty($referer)&&preg_match("/^((https|http):\/\/)?([^\/]+)/i", $referer, $matches)){
if (!empty($referer) && preg_match("/^((https|http):\/\/)?([^\/]+)/i", $referer, $matches)) {
$origin = $matches[0];
}
}
@ -21,7 +21,7 @@ class CORS
$response->header('Access-Control-Allow-Headers', 'Content-Type,X-Requested-With');
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', 10080);
return $response;
}
}
}

View File

@ -10,8 +10,8 @@ class Client
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)

View File

@ -9,9 +9,9 @@ class ForceJson
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)

View File

@ -10,9 +10,9 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)

View File

@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use App\Models\User;
class Server
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
return $next($request);
}
}

View File

@ -9,12 +9,19 @@ class User
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->input('access_token')) {
$user = \App\Models\User::where('token', $request->input('access_token'))->first();
if ($user) {
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
}
if (!$request->session()->get('id')) {
abort(403, '未登录或登陆已过期');
}

View File

@ -6,40 +6,6 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest
{
public static function filter() {
return [
'invite_force',
'invite_commission',
'invite_gen_limit',
'invite_never_expire',
'stop_register',
'email_verify',
'app_name',
'app_url',
'subscribe_url',
'plan_update_fee',
'plan_is_update',
// server
'server_token',
// alipay
'alipay_enable',
'alipay_appid',
'alipay_pubkey',
'alipay_privkey',
// stripe
'stripe_sk_live',
'stripe_pk_live',
'stripe_alipay_enable',
'stripe_wepay_enable',
'stripe_webhook_key',
// bitpayx,
'bitpayx_enable',
'bitpayx_appsecret',
// tutorial
'apple_id',
'apple_id_password'
];
}
/**
* Get the validation rules that apply to the request.
*
@ -48,36 +14,97 @@ class ConfigSave extends FormRequest
public function rules()
{
return [
// invite & commission
'safe_mode_enable' => 'in:0,1',
'invite_force' => 'in:0,1',
'invite_commission' => 'integer',
'invite_gen_limit' => 'integer',
'invite_never_expire' => 'in:0,1',
'commission_first_time_enable' => 'in:0,1',
'commission_auto_check_enable' => 'in:0,1',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_url' => 'url',
'subscribe_url' => 'url',
'plan_update_fee' => 'numeric',
'plan_is_update' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable|url',
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => '',
'email_gmail_limit_enable' => 'in:0,1',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'renew_reset_traffic_enable' => 'in:0,1',
// server
'server_token' => 'min:16',
'server_token' => 'nullable|min:16',
'server_license' => 'nullable',
'server_log_enable' => 'in:0,1',
'server_v2ray_domain' => '',
'server_v2ray_protocol' => '',
// alipay
'alipay_enable' => 'in:0,1',
'alipay_appid' => 'integer|min:16',
'alipay_appid' => 'nullable|integer|min:16',
'alipay_pubkey' => 'max:2048',
'alipay_privkey' => 'max:2048',
// stripe
'stripe_alipay_enable' => 'in:0,1',
'stripe_wepay_enable' => 'in:0,1',
'stripe_card_enable' => 'in:0,1',
'stripe_sk_live' => '',
'stripe_pk_live' => '',
'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
// bitpayx
'bitpayx_name' => '',
'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '',
// mGate
'mgate_name' => '',
'mgate_enable' => 'in:0,1',
'mgate_url' => 'nullable|url',
'mgate_app_id' => '',
'mgate_app_secret' => '',
// Epay
'epay_name' => '',
'epay_enable' => 'in:0,1',
'epay_url' => 'nullable|url',
'epay_pid' => '',
'epay_key' => '',
// frontend
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
// tutorial
'apple_id' => 'email'
'apple_id' => 'email',
'apple_id_password' => '',
// email
'email_template' => '',
'email_host' => '',
'email_port' => '',
'email_username' => '',
'email_password' => '',
'email_encryption' => '',
'email_from_address' => '',
// telegram
'telegram_bot_enable' => 'in:0,1',
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_id' => ''
];
}
public function messages()
{
// illiteracy prompt
return [
'app_url.url' => '站点URL格式不正确必须携带http(s)://',
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://'
];
}
}

View File

@ -0,0 +1,44 @@
<?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

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class MailSend extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'type' => 'required|in:1,2,3,4',
'subject' => 'required',
'content' => 'required',
'receiver' => 'array'
];
}
public function messages()
{
return [
'type.required' => '发送类型不能为空',
'type.in' => '发送类型格式有误',
'subject.required' => '主题不能为空',
'content.required' => '内容不能为空',
'receiver.array' => '收件人格式有误'
];
}
}

View File

@ -16,10 +16,10 @@ class NoticeSave extends FormRequest
return [
'title' => 'required',
'content' => 'required',
'img_url' => 'url'
'img_url' => 'nullable|url'
];
}
public function messages()
{
return [

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class OrderAssign extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'plan_id' => 'required',
'email' => 'required',
'total_amount' => 'required',
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price,onetime_price,reset_price'
];
}
public function messages()
{
return [
'plan_id.required' => '订阅不能为空',
'email.required' => '邮箱不能为空',
'total_amount.required' => '支付金额不能为空',
'cycle.required' => '订阅周期不能为空',
'cycle.in' => '订阅周期格式有误'
];
}
}

View File

@ -15,15 +15,15 @@ class OrderUpdate extends FormRequest
{
return [
'status' => 'in:0,1,2,3',
'commission_status' => 'in:0,1'
'commission_status' => 'in:0,1,2'
];
}
public function messages()
{
return [
'status.in' => '销售状态格式不正确',
'commission_status.in' => '续费状态格式不正确'
'commission_status.in' => '佣金状态格式不正确'
];
}
}

View File

@ -15,25 +15,32 @@ class PlanSave extends FormRequest
{
return [
'name' => 'required',
'content' => '',
'group_id' => 'required',
'transfer_enable' => 'required',
'month_price' => 'required|numeric',
'quarter_price' => 'required|numeric',
'half_year_price' => 'required|numeric',
'year_price' => 'required|numeric'
'month_price' => 'nullable|integer',
'quarter_price' => 'nullable|integer',
'half_year_price' => 'nullable|integer',
'year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer'
];
}
public function messages()
{
return [
'name.required' => '套餐名称不能为空',
'type.required' => '套餐类型不能为空',
'type.in' => '套餐类型格式有误',
'group_id.required' => '权限组不能为空',
'transfer_enable.required' => '流量不能为空',
'month_price.required' => '月付金额不能为空',
'quarter_price.required' => '季付金额不能为空',
'half_year_price.required' => '半年付金额不能为空',
'year_price.required' => '年付金额不能为空'
'month_price.integer' => '月付金额格式有误',
'quarter_price.integer' => '季付金额格式有误',
'half_year_price.integer' => '半年付金额格式有误',
'year_price.integer' => '年付金额格式有误',
'onetime_price.integer' => '一次性金额有误',
'reset_price.integer' => '流量重置包金额有误'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class PlanSort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'plan_ids' => 'required|array'
];
}
public function messages()
{
return [
'plan_ids.required' => '订阅计划ID不能为空',
'plan_ids.array' => '订阅计划ID格式有误'
];
}
}

View File

@ -18,7 +18,7 @@ class PlanUpdate extends FormRequest
'renew' => 'in:0,1'
];
}
public function messages()
{
return [

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerTrojanSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'allow_insecure' => 'nullable|in:0,1',
'server_name' => 'nullable',
'tags' => 'nullable|array',
'rate' => 'required|numeric'
];
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'parent_id.integer' => '父节点格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'allow_insecure.in' => '允许不安全格式不正确',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerTrojanSort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'server_ids' => 'required|array'
];
}
public function messages()
{
return [
'server_ids.required' => '服务器ID不能为空',
'server_ids.array' => '服务器ID格式有误'
];
}
}

View File

@ -4,21 +4,21 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerUpdate extends FormRequest
class ServerTrojanUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => 'in:0,1'
];
}
public function messages()
{
return [

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends FormRequest
class ServerV2raySave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,24 +14,31 @@ class ServerSave extends FormRequest
public function rules()
{
return [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required',
'tags' => 'array',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic'
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic',
'networkSettings' => '',
'ruleSettings' => '',
'tlsSettings' => '',
'dnsSettings' => ''
];
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'parent_id.integer' => '父ID格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerV2raySort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'server_ids' => 'required|array'
];
}
public function messages()
{
return [
'server_ids.required' => '服务器ID不能为空',
'server_ids.array' => '服务器ID格式有误'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerV2rayUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => 'in:0,1'
];
}
public function messages()
{
return [
'show.in' => '显示状态格式不正确'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class TutorialSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => 'required',
// 1:windows 2:macos 3:ios 4:android 5:linux 6:router
'category_id' => 'required|in:1,2,3,4,5,6',
'steps' => 'required'
];
}
public function messages()
{
return [
'title.required' => '标题不能为空',
'category_id.required' => '分类不能为空',
'category_id.in' => '分类格式不正确',
'steps.required' => '教程步骤不能为空'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class TutorialSort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'tutorial_ids' => 'required|array'
];
}
public function messages()
{
return [
'tutorial_ids.required' => '教程ID不能为空',
'tutorial_ids.array' => '教程ID格式有误'
];
}
}

View File

@ -15,15 +15,21 @@ class UserUpdate extends FormRequest
{
return [
'email' => 'required|email',
'password' => 'nullable',
'transfer_enable' => 'numeric',
'expired_at' => 'integer',
'expired_at' => 'nullable|integer',
'banned' => 'required|in:0,1',
'plan_id' => 'nullable|integer',
'commission_rate' => 'nullable|integer|min:0|max:100',
'discount' => 'nullable|integer|min:0|max:100',
'is_admin' => 'required|in:0,1',
'plan_id' => 'integer',
'commission_rate' => 'nullable|integer|min:0|max:100'
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer'
];
}
public function messages()
{
return [
@ -39,7 +45,15 @@ class UserUpdate extends FormRequest
'commission_rate.integer' => '推荐返利比例格式不正确',
'commission_rate.nullable' => '推荐返利比例格式不正确',
'commission_rate.min' => '推荐返利比例最小为0',
'commission_rate.max' => '推荐返利比例最大为100'
'commission_rate.max' => '推荐返利比例最大为100',
'discount.integer' => '专属折扣比例格式不正确',
'discount.nullable' => '专属折扣比例格式不正确',
'discount.min' => '专属折扣比例最小为0',
'discount.max' => '专属折扣比例最大为100',
'u.integer' => '上行流量格式不正确',
'd.integer' => '下行流量格式不正确',
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Passport;
use Illuminate\Foundation\Http\FormRequest;
class ForgetIndex extends FormRequest
class AuthForget extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -19,12 +19,12 @@ class ForgetIndex extends FormRequest
'email_code' => 'required'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数',
'email_code.required' => '邮箱验证码不能为空'

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Passport;
use Illuminate\Foundation\Http\FormRequest;
class LoginIndex extends FormRequest
class AuthLogin extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -18,12 +18,12 @@ class LoginIndex extends FormRequest
'password' => 'required|min:8'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
];

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Passport;
use Illuminate\Foundation\Http\FormRequest;
class RegisterIndex extends FormRequest
class AuthRegister extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -18,12 +18,12 @@ class RegisterIndex extends FormRequest
'password' => 'required|min:8'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
];

View File

@ -17,12 +17,12 @@ class CommSendEmailVerify extends FormRequest
'email' => 'required|email'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确'
'email.email' => '邮箱格式不正确'
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests;
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
@ -15,10 +15,10 @@ class OrderSave extends FormRequest
{
return [
'plan_id' => 'required',
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price'
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price,onetime_price,reset_price'
];
}
public function messages()
{
return [

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests;
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
@ -19,7 +19,7 @@ class TicketSave extends FormRequest
'message' => 'required'
];
}
public function messages()
{
return [

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class TicketWithdraw extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'withdraw_method' => 'required|in:alipay,paypal,usdt,btc',
'withdraw_account' => 'required'
];
}
public function messages()
{
return [
'withdraw_method.required' => '提现方式不能为空',
'withdraw_method.in' => '提现方式不支持',
'withdraw_account.required' => '提现账号不能为空'
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class UserChangePassword extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'old_password' => 'required',
'new_password' => 'required|min:8'
];
}
public function messages()
{
return [
'old_password.required' => '旧密码不能为空',
'new_password.required' => '新密码不能为空',
'new_password.min' => '密码必须大于8位数'
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests;
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
@ -18,7 +18,7 @@ class UserUpdate extends FormRequest
'remind_traffic' => 'in:0,1'
];
}
public function messages()
{
return [

View File

@ -0,0 +1,85 @@
<?php
namespace App\Http\Routes;
use Illuminate\Contracts\Routing\Registrar;
class AdminRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'admin',
'middleware' => 'admin'
], function ($router) {
// Config
$router->get ('/config/fetch', 'Admin\\ConfigController@fetch');
$router->post('/config/save', 'Admin\\ConfigController@save');
$router->get ('/config/getEmailTemplate', 'Admin\\ConfigController@getEmailTemplate');
$router->post('/config/setTelegramWebhook', 'Admin\\ConfigController@setTelegramWebhook');
// Plan
$router->get ('/plan/fetch', 'Admin\\PlanController@fetch');
$router->post('/plan/save', 'Admin\\PlanController@save');
$router->post('/plan/drop', 'Admin\\PlanController@drop');
$router->post('/plan/update', 'Admin\\PlanController@update');
$router->post('/plan/sort', 'Admin\\PlanController@sort');
// Server
$router->get ('/server/group/fetch', 'Admin\\Server\\GroupController@fetch');
$router->post('/server/group/save', 'Admin\\Server\\GroupController@save');
$router->post('/server/group/drop', 'Admin\\Server\\GroupController@drop');
$router->group([
'prefix' => 'server/trojan'
], function ($router) {
$router->get ('fetch', 'Admin\\Server\\TrojanController@fetch');
$router->post('save', 'Admin\\Server\\TrojanController@save');
$router->post('drop', 'Admin\\Server\\TrojanController@drop');
$router->post('update', 'Admin\\Server\\TrojanController@update');
$router->post('copy', 'Admin\\Server\\TrojanController@copy');
$router->post('sort', 'Admin\\Server\\TrojanController@sort');
$router->post('viewConfig', 'Admin\\Server\\TrojanController@viewConfig');
});
$router->group([
'prefix' => 'server/v2ray'
], function ($router) {
$router->get ('fetch', 'Admin\\Server\\V2rayController@fetch');
$router->post('save', 'Admin\\Server\\V2rayController@save');
$router->post('drop', 'Admin\\Server\\V2rayController@drop');
$router->post('update', 'Admin\\Server\\V2rayController@update');
$router->post('copy', 'Admin\\Server\\V2rayController@copy');
$router->post('sort', 'Admin\\Server\\V2rayController@sort');
$router->post('viewConfig', 'Admin\\Server\\V2rayController@viewConfig');
});
// 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');
// User
$router->get ('/user/fetch', 'Admin\\UserController@fetch');
$router->post('/user/update', 'Admin\\UserController@update');
$router->get ('/user/getUserInfoById', 'Admin\\UserController@getUserInfoById');
// Stat
$router->get ('/stat/getOverride', 'Admin\\StatController@getOverride');
// Notice
$router->get ('/notice/fetch', 'Admin\\NoticeController@fetch');
$router->post('/notice/save', 'Admin\\NoticeController@save');
$router->post('/notice/update', 'Admin\\NoticeController@update');
$router->post('/notice/drop', 'Admin\\NoticeController@drop');
// Ticket
$router->get ('/ticket/fetch', 'Admin\\TicketController@fetch');
$router->post('/ticket/reply', 'Admin\\TicketController@reply');
$router->post('/ticket/close', 'Admin\\TicketController@close');
// Mail
$router->post('/mail/send', 'Admin\\MailController@send');
// Coupon
$router->get ('/coupon/fetch', 'Admin\\CouponController@fetch');
$router->post('/coupon/save', 'Admin\\CouponController@save');
$router->post('/coupon/drop', 'Admin\\CouponController@drop');
// Tutorial
$router->get ('/tutorial/fetch', 'Admin\\TutorialController@fetch');
$router->post('/tutorial/save', 'Admin\\TutorialController@save');
$router->post('/tutorial/show', 'Admin\\TutorialController@show');
$router->post('/tutorial/drop', 'Admin\\TutorialController@drop');
$router->post('/tutorial/sort', 'Admin\\TutorialController@sort');
});
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Routes;
use Illuminate\Contracts\Routing\Registrar;
class ClientRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'client',
'middleware' => 'client'
], function ($router) {
// Client
$router->get('/subscribe', 'Client\\ClientController@subscribe');
// App
$router->get('/app/config', 'Client\\AppController@config');
$router->get('/app/getConfig', 'Client\\AppController@getConfig');
$router->get('/app/getVersion', 'Client\\AppController@getVersion');
});
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Routes;
use Illuminate\Contracts\Routing\Registrar;
class GuestRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'guest'
], function ($router) {
// Plan
$router->get ('/plan/fetch', 'Guest\\PlanController@fetch');
// Order
$router->post('/order/alipayNotify', 'Guest\\OrderController@alipayNotify');
$router->post('/order/stripeNotify', 'Guest\\OrderController@stripeNotify');
$router->post('/order/bitpayXNotify', 'Guest\\OrderController@bitpayXNotify');
$router->post('/order/mgateNotify', 'Guest\\OrderController@mgateNotify');
$router->post('/order/epayNotify', 'Guset\\OrderController@epayNotify');
// Telegram
$router->post('/telegram/webhook', 'Guest\\TelegramController@webhook');
});
}
}

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