852 Commits
1.1.1 ... 1.5.1

Author SHA1 Message Date
229a3022ac Merge pull request #439 from v2board/dev
1.5.1
2021-05-18 21:00:54 +09:00
554dc4c12b update: version 2021-05-18 21:00:01 +09:00
6175e59e6f remove: old route 2021-05-17 02:30:39 +09:00
9df6e52438 update: composer2 2021-05-17 02:30:00 +09:00
63acb4c581 fix: remark input 2021-05-16 23:16:33 +09:00
f54c82dcf9 remove: old code 2021-05-16 03:11:24 +09:00
b92b38a635 fix: stripe notify 2021-05-15 02:59:46 +09:00
ddab443d75 fix: payment input 2021-05-14 22:53:07 +09:00
8f806e6de7 update: chart smooth 2021-05-13 18:18:06 +09:00
f1c62e2732 knowledge: add access area 2021-05-13 18:08:09 +09:00
3275b96a0a fix: user tos target 2021-05-09 20:26:00 +09:00
240555104a Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-09 00:46:31 +09:00
30ff71bd11 payment: support epay 2021-05-09 00:46:21 +09:00
b17bbad745 Merge pull request #417 from ColetteContreras/patch-2 2021-05-07 14:16:51 +09:00
a301b8461e update: user 2021-05-07 12:07:20 +09:00
3e354bf5af update: admin 2021-05-07 01:27:54 +09:00
b9796f462c update: payment 2021-05-07 01:18:38 +09:00
dc6317db97 update 2021-05-07 01:17:55 +09:00
b3b0988730 update: tos config 2021-05-07 01:06:21 +09:00
4070761cd0 update: add tos 2021-05-07 00:56:54 +09:00
2c408a2f56 update 2021-05-07 00:15:30 +09:00
2cfaeb2fb9 add: tos url 2021-05-07 00:12:58 +09:00
ef1c0b6091 add: payment drop 2021-05-07 00:10:05 +09:00
8e1a313709 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-07 00:08:00 +09:00
d5cf1bc735 Merge pull request #407 from betaxab/p1 2021-05-03 13:32:19 +09:00
e1c63bd556 Merge pull request #412 from betaxab/p3 2021-05-03 13:31:07 +09:00
324e218767 update: frontent 2021-05-02 22:38:22 +09:00
04b5d16457 add: alipay f2f 2021-05-01 00:27:02 +09:00
2431ffaba7 update: payment 2021-04-29 01:39:45 +09:00
6b31c39e6e update: payment 2021-04-29 01:33:54 +09:00
2769a6adf4 update: stripe wepay 2021-04-29 01:27:24 +09:00
aa78761115 fix: payment 2021-04-29 01:25:11 +09:00
3e60cbf968 add: stripe wepay 2021-04-29 01:22:40 +09:00
84583bd384 update: payment 2021-04-28 18:48:18 +09:00
0333d62e6f fix: payment 2021-04-28 18:29:28 +09:00
3bfa2180e6 update 2021-04-28 18:00:22 +09:00
22ee741200 rewrite: payment 2021-04-28 17:56:08 +09:00
58b27cdd50 fix: hidden order fetch params 2021-04-15 22:41:29 +09:00
743311f2f7 Update PoseidonController.php
Fix server status
2021-04-06 07:03:45 +08:00
9c04f75b85 update: exchange api 2021-04-02 01:45:11 +09:00
c7d7dfda28 fix: more 2021-03-28 22:58:35 +09:00
35362689c4 update: config 2021-03-26 02:20:53 +09:00
b740998760 feature: customer service system 2021-03-25 18:26:22 +09:00
488eafdd67 feature: order filter 2021-03-25 17:59:28 +09:00
ff30d3dcb2 feature: server push status 2021-03-24 20:57:06 +09:00
cbe5882e01 feature: server push status 2021-03-24 16:04:09 +09:00
4e2e3cd2a0 rm: remove server name check 2021-03-22 02:33:33 +09:00
326fb5d918 fix: server name repeat 2021-03-20 02:24:55 +09:00
a49faf3e1f V2boardStatistics: comply with psr-4 autoloading standard 2021-03-19 18:28:58 +08:00
e4b76a705f fix: stat 2021-03-13 01:03:53 +09:00
a03125ee6a fix: stat 2021-03-12 01:24:50 +09:00
58a63ae819 add: server name check 2021-03-10 19:52:51 +09:00
ab8ebd593f URLSchemes: let's support standard v2ray vmess URL Schemes 2021-03-08 19:12:25 +08:00
11b6c8448a fix: user filter 2021-03-07 02:44:39 +09:00
9ce9af4917 fix: statistics 2021-03-06 01:22:37 +09:00
550f0fee37 update: admin user update 2021-03-02 21:12:32 +09:00
ab9c6c85bb fix: server rank 2021-02-26 23:54:38 +09:00
0e6f6358d8 opt v2board statistics 2021-02-23 14:49:07 +09:00
b0ddf7d45f opt 2021-02-19 01:15:37 +09:00
f8a851d464 Merge pull request #401 from v2board/dev
1.5.0
2021-02-17 17:20:51 +09:00
da8bff5609 update: frontend 2021-02-17 17:15:41 +09:00
d3150cadac update: version 2021-02-17 17:11:45 +09:00
cdddbae19a fix: something 2021-02-17 17:11:17 +09:00
7c40a146a9 update: app config 2021-02-16 01:53:37 +09:00
f81426b2c4 Merge pull request #399 from v2board/dev
1.4.3
2021-02-13 17:34:45 +09:00
e581d08f95 feature: more language switch 2021-02-06 00:03:58 +09:00
2ba924e578 update: withdraw close 2021-02-03 19:27:21 +09:00
77f6b3f289 feature: close withdraw 2021-02-03 19:17:32 +09:00
2979003b6f fix: get reset day 2021-02-03 19:03:28 +09:00
2be497b04d Merge pull request #388 from betaxab/p1 2021-02-03 13:08:17 +09:00
f55fc86547 fix: order chart 2021-02-01 19:04:03 +09:00
bc9b3b6e76 fix: server charts 2021-01-26 22:22:47 +09:00
fcafb65ce9 fix: config save 2021-01-26 22:20:27 +09:00
34be1b9579 fix: stat charts 2021-01-26 19:33:11 +09:00
627ff98882 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-01-21 23:29:34 +09:00
007b10d925 update: user remarks 2021-01-21 23:29:15 +09:00
f855ecc758 remove: test line 2021-01-21 15:52:12 +09:00
af8b2b61d4 language: update English translation
- minor Chinese entry adjustments
2021-01-21 00:50:56 +08:00
f7a7c21c16 update: backend language and order result page 2021-01-20 19:51:55 +09:00
edee396c9b fix: coupon generate 2021-01-15 00:35:36 +09:00
54d1226c8b fix: coupon generate 2021-01-15 00:33:30 +09:00
1ce019cfa5 update: commission 2021-01-13 12:24:22 +09:00
fe736df68d update: crisp and tawk info 2021-01-13 00:47:41 +09:00
72bf45e4d5 update: user 2021-01-12 00:49:59 +09:00
c5639a9bef update: other 2021-01-10 03:25:31 +09:00
3f1c505922 update: user 2021-01-08 14:21:13 +09:00
ec0fbabd07 update 2021-01-08 02:00:18 +09:00
eca105b7d1 update: frontend 2021-01-08 01:58:44 +09:00
a55647ab7a update: config 2021-01-08 01:33:11 +09:00
633b9ad912 Merge pull request #377 from wloot/patch-6
[security] Fix user info leak in getSubscribe()
2021-01-07 17:16:07 +09:00
be146bc98b Merge pull request #373 from betaxab/p1
subs: following shadowsocks SIP008 new changes
2021-01-07 17:11:11 +09:00
79cbe6ce39 Merge pull request #364 from wloot/patch-2
QuantumultX: keep expected behavior for host
2021-01-07 17:10:25 +09:00
f8185b51b7 update: more feature 2021-01-02 22:35:41 +09:00
b4d74a016a fix: traffic show 2021-01-02 12:07:28 +09:00
dd51daf9d8 [security] Fix user info leak in getSubscribe()
getSubscribe() leaks all user info even password hash, fix it.
2020-12-31 08:23:46 +08:00
4958c94bf2 update: user frontend 2020-12-28 17:23:56 +08:00
5766ec1951 update: statistics 2020-12-25 00:53:46 +08:00
faeaa02a84 compress 2020-12-24 23:49:52 +08:00
66c547d0ac update: more feature 2020-12-24 23:41:32 +08:00
d1cfc9815b subs: following shadowsocks SIP008 new changes
- do not use infix dereference operator to following the whole
2020-12-24 13:02:57 +08:00
b8b7033132 update: sql 2020-12-22 15:21:18 +08:00
9a465d9bb4 update: statistics 2020-12-21 17:11:48 +08:00
bcea2e7a54 update: statistics 2020-12-20 21:06:23 +08:00
7e5e696c70 update: statistics 2020-12-20 16:57:33 +08:00
1d70382aee update: db 2020-12-20 02:49:37 +08:00
8af862633f fix: frontend user edit 2020-12-19 21:33:11 +08:00
c0e60978ce fix: frontend user edit 2020-12-19 19:16:37 +08:00
f036b46bf0 QuantumultX: keep expected behavior for host
According to the documentation, quantumultx exposes one param for both sni and ws host. and sni priority is usually higher than ws host in other similar apps, we keep the behavior here.
ref: 8b6d7d84fc/sample.conf (L91)
2020-12-16 23:49:34 +08:00
a0d1b10614 fix: quantumultx tls-host key error 2020-12-16 23:15:20 +08:00
5fbefc7a98 Merge branch 'dev' 2020-12-14 11:03:57 +08:00
5a5491591b update: version 2020-12-14 11:03:32 +08:00
77e5eeea1b Merge branch 'dev' 2020-12-14 11:00:46 +08:00
da86b8cba9 update 2020-12-14 11:00:11 +08:00
91dfa5069c Merge pull request #361 from v2board/dev
1.4.2
2020-12-13 23:58:49 +08:00
2393ccbec7 update: server manage 2020-12-13 23:53:20 +08:00
962cfa9bea Merge pull request #359 from v2board/dev
1.4.2
2020-12-13 23:19:33 +08:00
8df4b6869a update: frontend 2020-12-13 23:15:47 +08:00
2159ab568a update: admin copy subscribe url 2020-12-13 23:09:32 +08:00
ef077cd095 update: frontend 2020-12-13 14:49:36 +08:00
17be643ce2 update: server manage 2020-12-13 14:16:07 +08:00
3b969c2c52 update: server manage 2020-12-13 12:17:48 +08:00
67d257c7b2 update: server manage 2020-12-13 12:16:05 +08:00
bc709a9c94 update: user filter 2020-12-11 22:32:10 +08:00
06f1ea0b81 update: user filter 2020-12-11 21:56:30 +08:00
0b425c1a01 update: admin 2020-12-11 20:45:54 +08:00
d072dc3e32 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-12-11 20:40:43 +08:00
db3e2abbbf update: user filter 2020-12-11 20:40:27 +08:00
c72e6cf4ea Merge pull request #356 from v2board/dev
1.4.1
2020-12-11 16:09:12 +08:00
b58df71db4 Merge pull request #355 from wloot/patch-1
Patch 1
2020-12-11 16:08:37 +08:00
91eb83388c Fix allowInsecure 2020-12-11 01:11:26 +08:00
16cf1f135a Fix allowInsecure 2020-12-11 01:11:04 +08:00
f3ef9e457e Fix allowInsecure 2020-12-11 01:09:49 +08:00
be1d22b423 Merge pull request #352 from betaxab/p1
rules: update default clash & surfboard & surge rules
2020-12-10 23:50:37 +08:00
ba2c92866c fix: mail knowledge link 2020-12-10 23:40:34 +08:00
91b016c231 update: log 2020-12-09 11:46:37 +08:00
d1b2e315ce update: log 2020-12-09 11:44:51 +08:00
8f0e3ce27d update: frontend 2020-12-08 18:59:07 +08:00
a47bac5ebc fix: ipad ua 2020-12-07 18:31:47 +08:00
d84af96535 update: app rules 2020-12-07 18:05:22 +08:00
29e7be855c rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-12-05 21:25:11 +08:00
1ca9899437 Merge pull request #345 from wloot/buildvmess
clean up buildvmess()
2020-12-05 17:16:41 +08:00
254fdf6049 update: reset day 2020-12-04 21:17:25 +08:00
896d9eb030 update: frontend 2020-12-03 21:29:13 +08:00
501986bf53 update: frontend 2020-12-03 14:16:47 +08:00
c83c7478b7 update: frontend 2020-11-30 14:07:04 +08:00
e0e9187655 update: frontend 2020-11-30 12:11:06 +08:00
8a894a9a32 update: reset day 2020-11-30 12:04:00 +08:00
f9e55a3905 update: remove coupon log 2020-11-28 23:04:56 +08:00
f81ecbea5d Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-28 23:04:45 +08:00
487bece1bb update: ui 2020-11-28 23:04:11 +08:00
5f0e62f43c Merge pull request #344 from betaxab/p1
subscription: add shadowsocks SIP008 subscription support
2020-11-27 01:48:50 +08:00
a8b6ebdc60 Merge pull request #333 from betaxab/p2
rules: update default clash & surfboard & surge rules
2020-11-27 01:48:00 +08:00
9af98f72fd update: check commission 2020-11-26 15:07:47 +08:00
a54f64b698 update: check commission 2020-11-26 14:44:00 +08:00
3e97b26593 rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-25 17:38:39 +08:00
cb2dd63e98 update: ui 2020-11-24 14:58:04 +08:00
512c06f15d update: ui 2020-11-24 00:57:27 +08:00
d645e9b98a update: app version 2020-11-22 16:25:56 +08:00
49c00c7a63 update: app version 2020-11-22 02:01:44 +08:00
7fe8a5b239 clean up buildvmess() 2020-11-20 00:53:53 +08:00
589eec9392 clean up buildvmess() 2020-11-20 00:30:49 +08:00
cada8ae9e8 update: app version 2020-11-19 21:30:27 +08:00
3ec90c4c52 update: app version 2020-11-19 21:27:53 +08:00
c1c6dccbfa update: app version 2020-11-19 21:24:14 +08:00
baf719fcff Merge remote-tracking branch 'origin/dev' into dev 2020-11-19 21:21:39 +08:00
7a9527d48a update: app version 2020-11-19 21:21:21 +08:00
7f12a79d07 subscription: add shadowsocks SIP008 subscription support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-19 13:05:49 +08:00
a0dcf51c28 Merge pull request #343 from ColetteContreras/fixPoseidon
Fix Poseidon, add ETag
2020-11-19 12:24:19 +08:00
6e4693534f Merge pull request #332 from betaxab/p1
OrderAssign: fixes can not assign two year & three year plan
2020-11-19 12:19:08 +08:00
d2bce02d4e Add ETag for caching 2020-11-18 05:10:32 -05:00
2b5ef08fe0 Fix alterid 2020-11-18 04:56:27 -05:00
732edcad57 update: fix opcache return null 2020-11-18 11:12:38 +08:00
e030ed8d80 update: custom v2ray alter id 2020-11-17 23:02:12 +08:00
717043c96a update: custom v2ray alter id 2020-11-17 22:18:00 +08:00
04250fb7ac update: custom v2ray alter id 2020-11-17 22:12:51 +08:00
d1e831e961 update: custom v2ray alter id 2020-11-17 21:24:42 +08:00
fb9465b5a6 update: custom v2ray alter id 2020-11-17 21:23:16 +08:00
ad98651f93 update: admin ui 2020-11-17 17:30:28 +08:00
9035afaa78 update: commission process 2020-11-17 17:01:02 +08:00
92e3e4e01f update: fix status 2020-11-17 16:40:13 +08:00
be19a93efb update: fix commission first 2020-11-17 16:34:10 +08:00
3aabeff5de update: fix guest 2020-11-16 22:22:23 +08:00
5889d527ad update: server 2020-11-16 11:58:34 +08:00
2063e3c51d update: quantumult 2020-11-15 23:42:58 +08:00
592b751e2c update: server 2020-11-15 17:10:32 +08:00
b4212e7af4 update: server 2020-11-15 15:25:32 +08:00
c7aa63759e update: frontend 2020-11-15 15:20:51 +08:00
b3f075ae42 update: server 2020-11-15 15:14:47 +08:00
6fd7b48d6f update: server 2020-11-15 15:14:19 +08:00
f1fd300a35 update: version 2020-11-15 12:45:35 +08:00
204982e4c5 update: frontend 2020-11-15 12:44:23 +08:00
5df67aef16 update: app 2020-11-14 23:04:11 +08:00
ad8dee359f fix: v2ray advance config 2020-11-14 23:02:49 +08:00
6ccb3fa7be update: frontend 2020-11-14 21:33:53 +08:00
3bf11a136a update: server sort 2020-11-14 17:41:26 +08:00
2fc7c8c07c clear: files 2020-11-14 17:27:54 +08:00
5b79ea569c update: server sort 2020-11-14 17:26:17 +08:00
9fa53efc0c update: sql 2020-11-13 02:17:19 +08:00
fb257c999f update: comm 2020-11-11 17:15:07 +08:00
cefb2b3a27 update: app 2020-11-10 21:20:18 +08:00
7263b6d0ed update: user filter 2020-11-10 21:08:57 +08:00
e6d5aadbd2 update: server submit 2020-11-09 20:44:03 +08:00
342415d1db OrderAssign: fixes can not assign two year & three year plan
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-09 09:26:07 +08:00
1788250dc8 update: server submit 2020-11-09 00:15:20 +08:00
9917e21a61 update: server submit 2020-11-09 00:12:19 +08:00
59a8429456 update: user manage 2020-11-07 18:01:23 +08:00
9345bf7979 update: order 2020-11-07 17:23:47 +08:00
7042dce0a1 update: traffic submit 2020-11-07 16:38:57 +08:00
675353bbe1 fix: bug 2020-11-07 16:05:26 +08:00
93c90ddaf0 fix: bug 2020-11-07 16:04:42 +08:00
1d92d6b2f9 fix: more bug 2020-11-07 15:44:57 +08:00
34b8b666f4 Merge branch 'master' into dev 2020-11-04 02:12:03 +08:00
1f9a8c807f fix: install sql 2020-11-04 02:11:44 +08:00
6d9ab7dfe2 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-02 22:22:48 +08:00
ebce59a0d1 fix: stat 2020-11-02 22:22:18 +08:00
c6be6b2fbc Merge pull request #327 from v2board/dev
1.4
2020-11-01 16:50:50 +08:00
e6f1bae7e9 Merge pull request #325 from betaxab/p1
ShadowsocksTidalabController: update name
2020-10-29 14:53:58 +08:00
8c777807a9 ShadowsocksTidalabController: update name
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-29 14:48:37 +08:00
2ee8a234a9 Merge pull request #316 from betaxab/p2
Utils: adjust QuantumultX & Surge & Surfboard
2020-10-29 14:42:09 +08:00
5cba4f2517 Merge pull request #324 from v2board/revert-323-patch-1
Revert "Fix Shadowsocks of Surfboard"
2020-10-29 14:41:57 +08:00
0d750af0cb Revert "Fix Shadowsocks of Surfboard" 2020-10-29 14:41:38 +08:00
0342ed69b6 Merge pull request #315 from betaxab/p1
dumpCSV: add BOM to prevent Chinese garbled in Excel
2020-10-29 14:40:12 +08:00
655b1dd49a Merge pull request #323 from Mazeorz/patch-1
Fix Shadowsocks of Surfboard
2020-10-29 14:39:31 +08:00
8935989b06 update: commission withdraw limit 2020-10-29 01:01:49 +08:00
9dc19baeb6 update: commission withdraw limit 2020-10-29 01:00:22 +08:00
c02db9d957 update: commission withdraw limit 2020-10-29 00:37:37 +08:00
5d6fc44281 Utils: adjust QuantumultX & Surge & Surfboard & Clash
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-27 09:37:02 +08:00
2bc77526fd Fix Shadowsocks of Surfboard
Modified the generated parameter format
2020-10-26 16:49:16 +08:00
06d22e7585 fix: reset password 2020-10-26 16:29:48 +08:00
645638feb9 fix: reset passwordk 2020-10-26 16:22:38 +08:00
ebec80a75c fix: expired buy reset issue 2020-10-26 15:50:37 +08:00
d654e01f95 fix: expired buy reset issue 2020-10-26 15:39:06 +08:00
be18727de1 update: staff 2020-10-25 14:18:48 +08:00
e82c5bc5ff update: config 2020-10-25 03:56:43 +08:00
69caf0d61c update: knowledge var 2020-10-25 00:41:27 +08:00
eab3cc48bd update: coupon 2020-10-25 00:04:36 +08:00
bf4e63ed9f update: add subscribe flag 2020-10-24 23:43:45 +08:00
4901cbfea5 update: coupon generate 2020-10-24 23:26:59 +08:00
1a94a48cf4 update: coupon generate 2020-10-24 23:08:14 +08:00
4ca1b9e8ff update: telegram notify 2020-10-20 23:11:59 +08:00
f2e7ada947 update: frontend 2020-10-20 11:28:45 +08:00
07cc8275d8 feature: multiple ban 2020-10-20 02:09:32 +08:00
16ae59c992 feature: new send mail 2020-10-19 00:37:27 +08:00
bbdda28197 update: knowledge 2020-10-18 20:07:26 +08:00
550a787c1a update: user filter 2020-10-18 19:21:21 +08:00
392c3677bc update: knowledge 2020-10-18 18:57:21 +08:00
28262568b2 update: knowledge 2020-10-18 16:38:11 +08:00
a2877b9a78 feature: knowledge base & surplus switch 2020-10-18 03:19:16 +08:00
5c1558beb9 feature: knowledge base & surplus switch 2020-10-18 02:53:56 +08:00
9041ee2a37 feature: knowledge base & surplus switch 2020-10-18 02:51:32 +08:00
be3e808551 update: dump csv 2020-10-14 19:42:08 +08:00
1116c7ef28 fix: token2login not working 2020-10-14 16:21:20 +08:00
eecb01c24e update: shell 2020-10-14 15:27:11 +08:00
7b4aec55b8 update: ja-jp 2020-10-14 00:52:47 +08:00
393680c963 feature: i18n 2020-10-13 23:55:17 +08:00
c984fa2e0b update: order service 2020-10-11 01:22:43 +08:00
8213dd3a73 update: admin frontend 2020-10-11 00:54:29 +08:00
56714617ea fix: reset traffic 2020-10-11 00:28:09 +08:00
00c5016d5a update: shadowsocks more cipher 2020-10-09 23:52:25 +08:00
ec4cd8acef update: google recaptcha 2020-10-09 23:34:34 +08:00
11adc63a3e update: google recaptcha 2020-10-09 23:30:12 +08:00
54ab44c3fd feature: google recaptcha 2020-10-09 23:27:36 +08:00
575bbe5174 dumpCSV: add BOM to prevent Chinese garbled in Excel
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-09 08:29:42 +08:00
3d26fda064 feature: dump csv 2020-10-09 00:50:10 +08:00
2ede0f3f17 feature: dump csv 2020-10-08 22:42:26 +08:00
a00eca2fda feature: dump csv 2020-10-08 22:35:07 +08:00
7a2967f41c feature: user filter & user generate 2020-10-08 12:13:47 +08:00
90545a333f Merge pull request #310 from betaxab/p1
Add Quantumult X & Surge Shadowsocks support
2020-10-08 12:12:40 +08:00
c6bf19ea75 Merge pull request #311 from betaxab/p2
subs: add Surfboard Shadowsocks support
2020-10-08 12:12:21 +08:00
c326d0ab5c subs: add Surfboard Shadowsocks support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-04 23:57:28 +08:00
1acf64db44 subs: add Quantumult X & Surge Shadowsocks support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-04 22:55:40 +08:00
87a9f6727b fix: shadowsocks server port 2020-10-04 16:26:28 +08:00
6fd577d2c8 update: support shadowsocks app subscribe 2020-10-04 16:07:19 +08:00
63adb9c4a8 fix: trojan urlencode issue 2020-10-04 15:35:42 +08:00
3be39043cb update: server 2020-10-04 14:52:33 +08:00
1f4778eaff update: server 2020-10-04 14:34:48 +08:00
ba2e0a6b66 feature: shadowsocks and more 2020-10-04 14:21:09 +08:00
ac48f90678 fix: remind traffic 2020-09-23 23:15:43 +08:00
76f0b4d4d0 fix: remind traffic 2020-09-23 22:40:20 +08:00
29fc4206c0 update: opcache fail abort 2020-09-23 14:37:24 +08:00
c8e6c79dd0 feature: staff permission 2020-09-20 16:41:48 +08:00
f0f636c722 feature: staff permission 2020-09-19 22:52:05 +08:00
d500769bd7 feature: fuzzy search 2020-09-18 14:18:56 +08:00
644aedb999 fix: order surplus issue 2020-09-16 20:44:50 +08:00
c8f1a23358 fix: order surplus issue 2020-09-16 20:31:30 +08:00
bb1a59291f fix: reset traffic 2020-09-14 17:43:47 +08:00
0cfa6a0676 fix: reset traffic 2020-09-14 17:41:26 +08:00
8aa3fb2f09 optimization: code 2020-09-11 01:22:55 +08:00
fe0332a9f9 optimization: code 2020-09-11 01:02:31 +08:00
e1cac79318 fix: cycle sort 2020-09-10 22:15:07 +08:00
85c4e477d2 feature: more cycle 2020-09-10 21:48:44 +08:00
8a44ccb3fc fix: onetime reset package 2020-09-10 13:08:12 +08:00
136c5cf9e9 fix: onetime change cycle invalid 2020-09-10 13:01:55 +08:00
2402a59b57 fix: tutorial apple id not null 2020-09-08 17:14:23 +08:00
c97451276a fix: var name 2020-09-08 13:58:58 +08:00
2a53e93444 readme: thanks jetbrains 2020-09-04 13:49:58 +08:00
20911fb669 fix: telegram multiple notification issues 2020-09-01 15:41:01 +08:00
31a222f3d8 fix: clash proxies merge 2020-09-01 10:08:42 +08:00
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
197 changed files with 10149 additions and 3558 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
.env.backup
.phpunit.result.cache
.idea
.lock
Homestead.json
Homestead.yaml
npm-debug.log

View File

@ -5,6 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CheckCommission extends Command
{
@ -38,19 +39,45 @@ 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('invite_user_id', '!=', NULL)
->whereNotIn('status', [0, 2])
->where('updated_at', '<=', strtotime('-3 day', time()))
->update([
'commission_status' => 1
]);
}
}
public function autoPayCommission()
{
$order = Order::where('commission_status', 1)
->where('status', 3)
->where('invite_user_id', '!=', NULL)
->get();
foreach ($order as $item) {
if ($item->invite_user_id) {
$inviter = User::find($item->invite_user_id);
if (!$inviter) continue;
if ((int)config('v2board.withdraw_close_enable', 0)) {
$inviter->balance = $inviter->balance + $item->commission_balance;
} else {
$inviter->commission_balance = $inviter->commission_balance + $item->commission_balance;
}
DB::beginTransaction();
if ($inviter->save()) {
$item->commission_status = 2;
$item->save();
if (!$item->save()) {
DB::rollBack();
continue;
}
DB::commit();
}
}
}

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()
{
$users = User::all();
foreach ($users as $user) {
if ($user->expired_at < time() || $user->u + $user->d >= $user->transfer_enable) {
$user->enable = 0;
} else {
$user->enable = 1;
}
$user->save();
}
}
}

View File

@ -2,12 +2,12 @@
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
{
@ -42,65 +42,21 @@ class CheckOrder extends Command
*/
public function handle()
{
$order = Order::get();
foreach ($order as $item) {
$orders = Order::get();
foreach ($orders as $item) {
$orderService = new OrderService($item);
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$item->status = 2;
$item->save();
$orderService->cancel();
}
break;
case 1:
$this->orderHandle($item);
$orderService->open();
break;
}
}
}
private function orderHandle($order)
{
$user = User::find($order->user_id);
return $this->buy($order, $user);
}
private function buy($order, $user)
{
$plan = Plan::find($order->plan_id);
// change plan process
if ((int)$order->type === 3 && (int)config('v2board.try_out_plan_id') !== (int)$user->plan_id) {
$transferEnableDifference = $plan->transfer_enable - ($user->transfer_enable / 1073741824);
$user->expired_at = $user->expired_at - ($transferEnableDifference * config('v2board.plan_transfer_hour', 12) * 3600);
}
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->enable = 1;
$user->u = 0;
$user->d = 0;
$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)
{
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);
}
}
}

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 $builder;
/**
* The name and signature of the console command.
*
@ -29,6 +30,8 @@ class ResetTraffic extends Command
public function __construct()
{
parent::__construct();
$this->builder = 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():void
{
$builder = $this->builder;
if ((string)date('d') === '01') {
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay():void
{
$builder = $this->builder;
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($builder->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

@ -5,7 +5,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
use App\Jobs\SendEmail;
use App\Jobs\SendEmailJob;
class SendRemindMail extends Command
{
@ -43,17 +43,16 @@ class SendRemindMail extends Command
$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 - 86400) < time() && $user->expired_at > time()) {
SendEmail::dispatch([
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' => 'mail.sendRemindExpire',
'template_name' => 'remindExpire',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
@ -61,31 +60,4 @@ class SendRemindMail extends Command
]);
}
}
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', 'mail.sendRemindTraffic')
->count();
if ($sendCount > 0) return;
SendEmail::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的流量使用已达到80%',
'template_name' => 'mail.sendRemindTraffic',
'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

@ -2,29 +2,24 @@
namespace App\Console\Commands;
use App\Services\PaymentService;
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\Cache;
class V2boardCache extends Command
class Test extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'v2board:cache';
protected $signature = 'test';
/**
* The console command description.
*
* @var string
*/
protected $description = '缓存任务';
protected $description = '';
/**
* Create a new command instance.
@ -43,5 +38,7 @@ class V2boardCache extends Command
*/
public function handle()
{
$paymentService = new PaymentService('MGate');
var_dump($paymentService->form());
}
}

View File

@ -114,7 +114,7 @@ class V2boardInstall extends Command
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();

View File

@ -0,0 +1,101 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StatServerJob;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\StatOrder;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
class V2boardStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'v2board:statistics';
/**
* 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->statOrder();
$this->statServer();
}
private function statOrder()
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$builder = Order::where('created_at', '>=', $startAt)
->where('created_at', '<', $endAt)
->whereNotIn('status', [0, 2]);
$orderCount = $builder->count();
$orderAmount = $builder->sum('total_amount');
$builder = $builder->where('commission_balance', '!=', 0)
->where('commission_status', 0);
$commissionCount = $builder->count();
$commissionAmount = $builder->sum('commission_balance');
$data = [
'order_count' => $orderCount,
'order_amount' => $orderAmount,
'commission_count' => $commissionCount,
'commission_amount' => $commissionAmount,
'record_type' => 'd',
'record_at' => $startAt
];
$statistic = StatOrder::where('record_at', $startAt)
->where('record_type', 'd')
->first();
if ($statistic) {
$statistic->update($data);
return;
}
StatOrder::create($data);
}
private function statServer()
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$statistics = ServerLog::select([
'server_id',
'method as server_type',
DB::raw("sum(u) as u"),
DB::raw("sum(d) as d"),
])
->where('log_at', '>=', $startAt)
->where('log_at', '<', $endAt)
->groupBy('server_id', 'method')
->get()
->toArray();
foreach ($statistics as $statistic) {
$statistic['record_type'] = 'd';
$statistic['record_at'] = $startAt;
StatServerJob::dispatch($statistic);
}
}
}

View File

@ -25,14 +25,13 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// v2board
$schedule->command('v2board:cache')->hourly();
$schedule->command('v2board:statistics')->daily();
// check
$schedule->command('check:order')->everyMinute();
$schedule->command('check:expire')->everyMinute();
$schedule->command('check:commission')->everyMinute();
// reset
$schedule->command('reset:traffic')->monthly();
$schedule->command('reset:serverLog')->monthly();
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->quarterly();
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
}

View File

@ -46,6 +46,9 @@ class Handler extends ExceptionHandler
*/
public function render($request, Exception $exception)
{
if($exception instanceof \Illuminate\Http\Exceptions\ThrottleRequestsException) {
abort(429, '请求频繁,请稍后再试');
}
return parent::render($request, $exception);
}

View File

@ -3,12 +3,38 @@
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;
class ConfigController extends Controller
{
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 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
@ -18,7 +44,12 @@ class ConfigController extends Controller
'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),
'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100),
'commission_withdraw_method' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0)
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
@ -28,12 +59,21 @@ class ConfigController extends Controller
'app_description' => config('v2board.app_description', 'V2Board is best!'),
'app_url' => config('v2board.app_url'),
'subscribe_url' => config('v2board.subscribe_url'),
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
'plan_transfer_hour' => config('v2board.plan_transfer_hour', 12),
'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_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'tos_url' => config('v2board.tos_url')
],
'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', 0),
'surplus_enable' => (int)config('v2board.surplus_enable', 1)
],
'pay' => [
// alipay
@ -42,29 +82,69 @@ 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_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// paytaro
'paytaro_enable' => config('v2board.paytaro_enable'),
'paytaro_app_id' => config('v2board.paytaro_app_id'),
'paytaro_app_secret' => config('v2board.paytaro_app_secret')
// 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' => config('v2board.frontend_theme', 1),
'frontend_background_url' => config('v2board.frontend_background_url')
'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'),
'frontend_customer_service_method' => config('v2board.frontend_customer_service_method', 0),
'frontend_customer_service_id' => config('v2board.frontend_customer_service_id'),
],
'server' => [
'server_token' => config('v2board.server_token'),
'server_license' => config('v2board.server_license')
'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')
],
'app' => [
'windows_version' => config('v2board.windows_version'),
'windows_download_url' => config('v2board.windows_download_url'),
'macos_version' => config('v2board.macos_version'),
'macos_download_url' => config('v2board.macos_download_url'),
'android_version' => config('v2board.android_version'),
'android_download_url' => config('v2board.android_download_url')
]
]
]);
@ -75,7 +155,7 @@ class ConfigController extends Controller
$data = $request->input();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, ConfigSave::filter())) {
if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改');
}
$array[$k] = $v;
@ -84,6 +164,11 @@ class ConfigController extends Controller
if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
if (opcache_reset() === false) {
abort(500, '缓存清除失败请卸载或检查opcache配置状态');
}
}
\Artisan::call('config:cache');
return response([
'data' => true

View File

@ -3,38 +3,54 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\CouponSave;
use App\Http\Requests\Admin\CouponGenerate;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
foreach ($coupons as $k => $v) {
if ($coupons[$k]['limit_plan_ids']) $coupons[$k]['limit_plan_ids'] = json_decode($coupons[$k]['limit_plan_ids']);
}
return response([
'data' => Coupon::all()
'data' => $coupons,
'total' => $total
]);
}
public function save(CouponSave $request)
{
$params = $request->only([
'name',
'type',
'value',
'started_at',
'ended_at',
'limit_use'
]);
$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 {
if (!Coupon::find($request->input('id'))->update($params)) {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
@ -44,6 +60,70 @@ class CouponController extends Controller
]);
}
public function generate(CouponGenerate $request)
{
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$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
]);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
if (isset($coupon['limit_plan_ids'])) {
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
}
$coupon['created_at'] = $coupon['updated_at'] = time();
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
array_push($coupons, $coupon);
}
DB::beginTransaction();
if (!Coupon::insert($coupons)) {
DB::rollBack();
abort(500, '生成失败');
}
DB::commit();
$data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n";
foreach($coupons as $coupon) {
$type = ['', '金额', '比例'][$coupon['type']];
$value = ['', ($coupon['value'] / 100),$coupon['value']][$coupon['type']];
$startTime = date('Y-m-d H:i:s', $coupon['started_at']);
$endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
$limitUse = $coupon['limit_use'] ?? '不限制';
$createTime = date('Y-m-d H:i:s', $coupon['created_at']);
$limitPlanIds = $coupon['limit_plan_ids'] ?? '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {

View File

@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\KnowledgeSave;
use App\Http\Requests\Admin\KnowledgeSort;
use App\Models\Knowledge;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::find($request->input('id'))->toArray();
if (!$knowledge) abort(500, '知识不存在');
return response([
'data' => $knowledge
]);
}
return response([
'data' => Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get()
]);
}
public function getCategory(Request $request)
{
return response([
'data' => array_keys(Knowledge::get()->groupBy('category')->toArray())
]);
}
public function save(KnowledgeSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Knowledge::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, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
$knowledge->show = $knowledge->show ? 0 : 1;
if (!$knowledge->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(KnowledgeSort $request)
{
DB::beginTransaction();
foreach ($request->input('knowledge_ids') as $k => $v) {
if (!Knowledge::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, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
if (!$knowledge->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\MailSend;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Jobs\SendEmail;
class MailController extends Controller
{
public function send(MailSend $request)
{
switch ($request->input('type')) {
case 1: $users = $this->getAllUser();
break;
case 2: $users = $this->getReceiver($request->input('receiver'));
break;
case 3: $users = $this->getSubscribeUser();
break;
case 4: $users = $this->getExpireUser();
break;
}
foreach ($users as $user) {
SendEmail::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'mail.sendEmailCustom',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
])->onQueue('other_mail');
}
return response([
'data' => true
]);
}
private function getAllUser()
{
return User::all();
}
private function getReceiver($receiver)
{
if (empty($receiver)) {
abort(500, '收件人不能为空');
}
return User::whereIn('id', $receiver)->get();
}
private function getSubscribeUser()
{
return User::where('expired_at', '=>', time())->get();
}
private function getExpireUser()
{
return User::where('expired_at', '<', time())->get();
}
}

View File

@ -29,7 +29,9 @@ class NoticeController extends Controller
abort(500, '保存失败');
}
} else {
if (!Notice::find($request->input('id'))->update($data)) {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}

View File

@ -2,33 +2,50 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Http\Requests\Admin\OrderFetch;
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)
private function filter(Request $request, &$builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'email') {
$user = User::where('email', "%{$filter['value']}%")->first();
if (!$user) continue;
$builder->where('user_id', $user->id);
continue;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function fetch(OrderFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$orderModel = Order::orderBy('created_at', 'DESC');
if ($request->input('trade_no')) {
$orderModel->where('trade_no', $request->input('trade_no'));
}
if ($request->input('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->where('status', 3);
}
if ($request->input('id')) {
$orderModel->where('id', $request->input('id'));
}
if ($request->input('user_id')) {
$orderModel->where('user_id', $request->input('user_id'));
$orderModel->whereNotIn('status', [0, 2]);
$orderModel->where('commission_balance', '>', 0);
}
$this->filter($request, $orderModel);
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->get();
@ -48,7 +65,7 @@ class OrderController extends Controller
public function update(OrderUpdate $request)
{
$updateData = $request->only([
$params = $request->only([
'status',
'commission_status'
]);
@ -59,7 +76,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, '更新失败');
}
@ -87,4 +116,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

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Payment;
class PaymentController extends Controller
{
public function getPaymentMethods()
{
$methods = [];
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
array_push($methods, pathinfo($file)['filename']);
}
return response([
'data' => $methods
]);
}
public function fetch()
{
$payments = Payment::all();
foreach ($payments as $k => $v) {
$payments[$k]['notify_url'] = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
}
return response([
'data' => $payments
]);
}
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return response([
'data' => $paymentService->form()
]);
}
public function save(Request $request)
{
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($request->input());
} catch (\Exception $e) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
if (!Payment::create([
'name' => $request->input('name'),
'payment' => $request->input('payment'),
'config' => $request->input('config'),
'uuid' => Helper::guid()
])) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
return response([
'data' => $payment->delete()
]);
}
}

View File

@ -3,43 +3,73 @@
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)
{
$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)
{
$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, '保存失败');
}
$plan->name = $request->input('name');
$plan->content = $request->input('content');
$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');
DB::commit();
return response([
'data' => $plan->save()
'data' => true
]);
}
if (!Plan::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
@ -73,7 +103,10 @@ class PlanController extends Controller
if (!$plan) {
abort(500, '该订阅不存在');
}
if (!$plan->update($updateData)) {
try {
$plan->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
}
@ -81,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,61 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Models\Server;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
$serverService = new ServerService();
$servers = array_merge(
$serverService->getShadowsocksServers(),
$serverService->getV2rayServers(),
$serverService->getTrojanServers()
);
$serverService->mergeData($servers);
$tmp = array_column($servers, 'sort');
array_multisort($tmp, SORT_ASC, $servers);
return response([
'data' => $servers
]);
}
public function sort(Request $request)
{
DB::beginTransaction();
foreach ($request->input('sorts') as $k => $v) {
switch ($v['key']) {
case 'shadowsocks':
if (!ServerShadowsocks::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
case 'v2ray':
if (!Server::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
case 'trojan':
if (!ServerTrojan::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerShadowsocksSave;
use App\Http\Requests\Admin\ServerShadowsocksUpdate;
use App\Models\ServerShadowsocks;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ShadowsocksController extends Controller
{
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerShadowsocks::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerShadowsocksUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerShadowsocks::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 = ServerShadowsocks::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerShadowsocks::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerTrojan;
class TrojanController extends Controller
{
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
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 viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getTrojanConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerV2raySave;
use App\Http\Requests\Admin\ServerV2rayUpdate;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Server;
class V2rayController extends Controller
{
public function save(ServerV2raySave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if (isset($params['dnsSettings'])) {
if (!is_object(json_decode($params['dnsSettings']))) {
abort(500, 'DNS规则配置格式不正确');
}
}
if (isset($params['ruleSettings'])) {
if (!is_object(json_decode($params['ruleSettings']))) {
abort(500, '审计规则配置格式不正确');
}
}
if (isset($params['networkSettings'])) {
if (!is_object(json_decode($params['networkSettings']))) {
abort(500, '传输协议配置格式不正确');
}
}
if (isset($params['tlsSettings'])) {
if (!is_object(json_decode($params['tlsSettings']))) {
abort(500, 'TLS配置格式不正确');
}
}
if ($request->input('id')) {
$server = Server::find($request->input('id'));
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->getV2RayConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
}

View File

@ -1,167 +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\Cache;
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']);
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get('server_last_check_at_' . $server[$i]['parent_id']);
} else {
$server[$i]['last_check_at'] = Cache::get('server_last_check_at_' . $server[$i]['id']);
}
}
return response([
'data' => $server
]);
}
public function save(ServerSave $request)
{
$params = $request->only(array_keys(ServerSave::RULES));
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if (isset($params['rules'])) {
if (!is_object(json_decode($params['rules']))) {
abort(500, '审计规则配置格式不正确');
}
}
if (isset($params['settings'])) {
if (!is_object(json_decode($params['settings']))) {
abort(500, '传输协议配置格式不正确');
}
}
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
if (!$server->update($params)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!Server::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
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)
{
$params = $request->only([
'show',
]);
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
if (!$server->update($params)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
}

View File

@ -2,6 +2,9 @@
namespace App\Http\Controllers\Admin;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerGroup;
@ -10,7 +13,10 @@ use App\Models\Plan;
use App\Models\User;
use App\Models\Ticket;
use App\Models\Order;
use App\Models\StatOrder;
use App\Models\StatServer;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
@ -20,7 +26,7 @@ class StatController extends Controller
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->where('status', '3')
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
@ -29,9 +35,91 @@ class StatController extends Controller
->count(),
'commission_pendding_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->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')))
->whereNotIn('status', [0, 2])
->sum('total_amount')
]
]);
}
public function getOrder(Request $request)
{
$statistics = StatOrder::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
array_push($result, [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['order_amount'] / 100
]);
array_push($result, [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['order_count']
]);
array_push($result, [
'type' => '佣金金额',
'date' => $date,
'value' => $statistic['commission_amount'] / 100
]);
array_push($result, [
'type' => '佣金笔数',
'date' => $date,
'value' => $statistic['commission_count']
]);
}
$result = array_reverse($result);
return response([
'data' => $result
]);
}
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::where('parent_id', null)->get()->toArray(),
'vmess' => Server::where('parent_id', null)->get()->toArray(),
'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray()
];
$timestamp = strtotime('-1 day', strtotime(date('Y-m-d')));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $timestamp)
->where('record_type', 'd')
->limit(10)
->orderBy('total', 'DESC')
->get()
->toArray();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return response([
'data' => $statistics
]);
}
}

View File

@ -2,10 +2,14 @@
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
@ -30,17 +34,25 @@ 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
]);
}
@ -52,26 +64,12 @@ class TicketController extends Controller
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
]);

View File

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\TutorialSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Tutorial;
class TutorialController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Tutorial::all()
]);
}
public function save(TutorialSave $request)
{
$params = $request->only([
'title',
'description',
'steps',
'icon'
]);
if (!$request->input('id')) {
if (!Tutorial::create($params)) {
abort(500, '创建失败');
}
} else {
if (!Tutorial::find($request->input('id'))->update($params)) {
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 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

@ -2,28 +2,62 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\UserFetch;
use App\Http\Requests\Admin\UserGenerate;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Admin\UserUpdate;
use App\Jobs\SendEmailJob;
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 UserController extends Controller
{
public function fetch(Request $request)
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) abort(500, '用户不存在');
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
return response([
'data' => $user->save()
]);
}
private function filter(Request $request, $builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['value'])->first();
if (!$user) continue;
$builder->where('invite_user_id', $user->id);
continue;
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function fetch(UserFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$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'));
}
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->get();
@ -34,6 +68,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
}
return response([
'data' => $res,
@ -47,57 +82,213 @@ class UserController extends Controller
abort(500, '参数错误');
}
return response([
'data' => User::select([
'email',
'u',
'd',
'transfer_enable',
'expired_at'
])->find($request->input('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',
'discount',
'is_admin',
'u',
'd',
'balance',
'commission_balance'
]);
$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']);
}
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([
'data' => true
]);
}
public function dumpCSV(Request $request)
{
$userModel = User::orderBy('id', 'asc');
$this->filter($request, $userModel);
$res = $userModel->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($res as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$balance = $user['balance'] / 100;
$commissionBalance = $user['commission_balance'] / 100;
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅';
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
}
public function generate(UserGenerate $request)
{
if ($request->input('email_prefix')) {
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$user = [
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
abort(500, '生成失败');
}
return response([
'data' => true
]);
}
if ($request->input('generate_count')) {
$this->multiGenerate($request);
}
}
private function multiGenerate(Request $request)
{
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$users = [];
for ($i = 0;$i < $request->input('generate_count');$i++) {
$user = [
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid(),
'created_at' => time(),
'updated_at' => time()
];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
DB::beginTransaction();
if (!User::insert($users)) {
DB::rollBack();
abort(500, '生成失败');
}
DB::commit();
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($users as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email'];
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
}
public function sendMail(UserSendMail $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
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
]);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
public function setInviteUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer',
'invite_user' => 'required',
], [
'user_id.required' => '用户ID不能为空',
'user_id.integer' => '用户ID参数有误',
'invite_user.required' => '邀请人不能为空'
]);
$user = User::find($request->input('user_id'));
if (!$user) abort(500, '用户不存在');
if (strpos($request->input('invite_user'), '@') !== -1) {
$inviteUser = User::where('email', $request->input('invite_user'))->first();
} else {
$inviteUser = User::find($request->input('invite_user'));
}
if (!$inviteUser) abort(500, '邀请人不存在');
$user->invite_user_id = $inviteUser->id;
return response([
'data' => $user->save()
]);
}
}

View File

@ -3,12 +3,12 @@
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\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\Notice;
use App\Utils\Helper;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
@ -16,37 +16,71 @@ class AppController extends Controller
CONST SOCKS_PORT = 10010;
CONST HTTP_PORT = 10011;
// TODO: 1.1.1 abolish
public function data(Request $request)
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$nodes = [];
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, '订阅计划不存在');
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
}
if ($user->expired_at > time()) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
$config = Yaml::parseFile(base_path() . '/resources/rules/app.clash.yaml');
$proxy = [];
$proxies = [];
foreach ($servers as $item) {
$groupId = json_decode($item['group_id']);
if (in_array($user->group_id, $groupId)) {
array_push($nodes, $item);
if ($item['type'] === 'shadowsocks') {
array_push($proxy, Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'v2ray') {
array_push($proxy, Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
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(Request $request)
{
if (strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
|| strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
) {
if (strpos($request->header('user-agent'), 'Win64') !== false) {
return response([
'data' => [
'version' => config('v2board.windows_version'),
'download_url' => config('v2board.windows_download_url')
]
]);
} else {
return response([
'data' => [
'version' => config('v2board.macos_version'),
'download_url' => config('v2board.macos_download_url')
]
]);
}
return;
}
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()
'windows_version' => config('v2board.windows_version'),
'windows_download_url' => config('v2board.windows_download_url'),
'macos_version' => config('v2board.macos_version'),
'macos_download_url' => config('v2board.macos_download_url'),
'android_version' => config('v2board.android_version'),
'android_download_url' => config('v2board.android_download_url')
]
]);
}
@ -57,7 +91,7 @@ class AppController extends Controller
abort(500, '参数错误');
}
$user = $request->user;
if ($user->expired_at < time()) {
if ($user->expired_at < time() && $user->expired_at !== NULL) {
abort(500, '订阅计划已过期');
}
$server = Server::where('show', 1)
@ -74,29 +108,29 @@ class AppController extends Controller
//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]->users[0]->id = (string)$user->uuid;
$json->outbound->settings->vnext[0]->users[0]->alterId = (int)$server->alter_id;
$json->outbound->settings->vnext[0]->remark = (string)$server->name;
$json->outbound->streamSettings->network = $server->network;
if ($server->settings) {
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->outbound->streamSettings->tcpSettings = json_decode($server->settings);
$json->outbound->streamSettings->tcpSettings = json_decode($server->networkSettings);
break;
case 'kcp':
$json->outbound->streamSettings->kcpSettings = json_decode($server->settings);
$json->outbound->streamSettings->kcpSettings = json_decode($server->networkSettings);
break;
case 'ws':
$json->outbound->streamSettings->wsSettings = json_decode($server->settings);
$json->outbound->streamSettings->wsSettings = json_decode($server->networkSettings);
break;
case 'http':
$json->outbound->streamSettings->httpSettings = json_decode($server->settings);
$json->outbound->streamSettings->httpSettings = json_decode($server->networkSettings);
break;
case 'domainsocket':
$json->outbound->streamSettings->dsSettings = json_decode($server->settings);
$json->outbound->streamSettings->dsSettings = json_decode($server->networkSettings);
break;
case 'quic':
$json->outbound->streamSettings->quicSettings = json_decode($server->settings);
$json->outbound->streamSettings->quicSettings = json_decode($server->networkSettings);
break;
}
}

359
app/Http/Controllers/Client/ClientController.php Executable file → Normal file
View File

@ -3,159 +3,280 @@
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 App\Utils\Surfboard;
use App\Utils\URLSchemes;
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)
{
$flag = $request->input('flag')
?? (isset($_SERVER['HTTP_USER_AGENT'])
? $_SERVER['HTTP_USER_AGENT']
: '');
$flag = strtolower($flag);
$user = $request->user;
$server = [];
// account not expired and is not banned.
if ($user->expired_at > time() && !$user->banned) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
if ($flag) {
if (strpos($flag, 'quantumult%20x') !== false) {
die($this->quantumultX($user, $servers));
}
if (strpos($flag, 'quantumult') !== false) {
die($this->quantumult($user, $servers));
}
if (strpos($flag, 'clash') !== false) {
die($this->clash($user, $servers));
}
if (strpos($flag, 'surfboard') !== false) {
die($this->surfboard($user, $servers));
}
if (strpos($flag, 'surge') !== false) {
die($this->surge($user, $servers));
}
if (strpos($flag, 'shadowrocket') !== false) {
die($this->shadowrocket($user, $servers));
}
if (strpos($flag, 'shadowsocks') !== false) {
die($this->shaodowsocksSIP008($user, $servers));
}
}
die($this->origin($user, $servers));
}
}
// TODO: Ready to stop support
private function quantumult($user, $servers = [])
{
$uri = '';
header('subscription-userinfo: upload=' . $user['u'] . '; download=' . $user['d'] . ';total=' . $user['transfer_enable']);
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 (isset($wsSettings->path)) $uri .= ', obfs-uri=' . $wsSettings->path;
if (isset($wsSettings->headers->Host)) $uri .= ', obfs-host=' . $wsSettings->headers->Host;
}
}
$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) {
if ($item['type'] === 'v2ray') {
$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 .= $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->settings) {
$wsSettings = json_decode($item->settings);
if (isset($wsSettings->path)) $str .= ', obfs-path="' . $wsSettings->path . '"';
if (isset($wsSettings->headers->Host)) $str .= ', obfs-header="Host:' . $wsSettings->headers->Host . '"';
if ($item['networkSettings']) {
$wsSettings = json_decode($item['networkSettings'], true);
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 origin($user, $server)
private function shadowrocket($user, $servers = [])
{
$uri = '';
foreach ($server as $item) {
$uri .= Helper::buildVmessLink($item, $user);
//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 ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= Shadowrocket::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= Shadowrocket::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= Shadowrocket::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
private function clash($user, $server)
private function quantumultX($user, $servers = [])
{
$uri = '';
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= QuantumultX::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= QuantumultX::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= QuantumultX::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
private function origin($user, $servers = [])
{
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= URLSchemes::buildShadowsocks($item, $user);
}
if ($item['type'] === 'v2ray') {
$uri .= URLSchemes::buildVmess($item, $user);
}
if ($item['type'] === 'trojan') {
$uri .= URLSchemes::buildTrojan($item, $user);
}
}
return base64_encode($uri);
}
private function shaodowsocksSIP008($user, $servers = [])
{
$configs = [];
$subs = [];
$subs['servers'] = [];
$subs['bytes_used'] = '';
$subs['bytes_remaining'] = '';
$bytesUsed = $user['u'] + $user['d'];
$bytesRemaining = $user['transfer_enable'] - $bytesUsed;
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($configs, URLSchemes::buildShadowsocksSIP008($item, $user));
}
}
$subs['version'] = 1;
$subs['bytes_used'] = $bytesUsed;
$subs['bytes_remaining'] = $bytesRemaining;
$subs['servers'] = array_merge($subs['servers'] ? $subs['servers'] : [], $configs);
return json_encode($subs, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
}
private function surge($user, $servers = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= Surge::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= Surge::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [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, $servers = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= Surfboard::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= Surfboard::buildVmess($user['uuid'], $item);
// [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, $servers = [])
{
$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 = [];
$proxyGroup = [];
$proxies = [];
$rules = [];
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;
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item->network == 'ws') {
$array['network'] = $item->network;
if ($item->settings) {
$wsSettings = json_decode($item->settings);
if (isset($wsSettings->path)) $array['ws-path'] = $wsSettings->path;
if (isset($wsSettings->headers->Host)) $array['ws-headers'] = [
'Host' => $wsSettings->headers->Host
];
if ($item['type'] === 'v2ray') {
array_push($proxy, Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, Clash::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']);
}
array_push($proxy, $array);
array_push($proxies, $item->name);
}
array_push($proxyGroup, [
'name' => 'auto',
'type' => 'url-test',
'proxies' => $proxies,
'url' => 'https://www.bing.com',
'interval' => 300
]);
array_push($proxyGroup, [
'name' => 'fallback-auto',
'type' => 'fallback',
'proxies' => $proxies,
'url' => 'https://www.bing.com',
'interval' => 300
]);
array_push($proxyGroup, [
'name' => 'select',
'type' => 'select',
'proxies' => $proxies
]);
try {
$rules = Yaml::parseFile(base_path() . '/resources/rules/clash.rule.yaml')['Rule'];
} catch (\Exception $e) {}
$config = [
'port' => 7890,
'socks-port' => 7891,
'allow-lan' => false,
'mode' => 'Rule',
'log-level' => 'info',
'external-controller' => '0.0.0.0:9090',
'secret' => '',
'Proxy' => $proxy,
'Proxy Group' => $proxyGroup,
'Rule' => $rules
];
return Yaml::dump($config);
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
if (!is_array($config['proxy-groups'][$k]['proxies'])) continue;
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'tos_url' => config('v2board.tos_url')
]
]);
}
}

View File

@ -1,150 +0,0 @@
<?php
namespace App\Http\Controllers\Guest;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Omnipay\Omnipay;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Library\BitpayX;
use Library\PayTaro;
class OrderController extends Controller
{
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'));
$gateway->setPrivateKey(config('v2board.alipay_privkey')); // 可以是路径,也可以是密钥内容
$gateway->setAlipayPublicKey(config('v2board.alipay_pubkey')); // 可以是路径,也可以是密钥内容
$request = $gateway->completePurchase();
$request->setParams($_POST); //Optional
try {
/** @var \Omnipay\Alipay\Responses\AopCompletePurchaseResponse $response */
$response = $request->send();
if ($response->isPaid()) {
/**
* Payment is successful
*/
if (!$this->handle($_POST['out_trade_no'], $_POST['trade_no'])) {
abort(500, 'fail');
}
die('success'); //The response should be 'success' only
} else {
/**
* Payment is not successful
*/
die('fail');
}
} catch (Exception $e) {
/**
* Payment is not successful
*/
die('fail');
}
}
public function stripeNotify(Request $request)
{
Log::info('stripeNotifyData: ' . json_encode($request->input()));
\Stripe\Stripe::setApiKey(config('v2board.stripe_sk_live'));
try {
$event = \Stripe\Webhook::constructEvent(
file_get_contents('php://input'),
$_SERVER['HTTP_STRIPE_SIGNATURE'],
config('v2board.stripe_webhook_key')
);
} catch (\Stripe\Error\SignatureVerification $e) {
abort(400);
}
switch ($event->type) {
case 'source.chargeable':
$source = $event->data->object;
$charge = \Stripe\Charge::create([
'amount' => $source['amount'],
'currency' => $source['currency'],
'source' => $source['id'],
]);
if ($charge['status'] == 'succeeded') {
$trade_no = Cache::get($source['id']);
if (!$trade_no) {
abort(500, 'redis is not found trade no by stripe source id');
}
if (!$this->handle($trade_no, $source['id'])) {
abort(500, 'fail');
}
Cache::forget($source['id']);
die('success');
}
break;
default:
abort(500, 'event is not support');
}
}
public function bitpayXNotify(Request $request)
{
$inputString = file_get_contents('php://input', 'r');
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']
];
$strToSign = $bitpayX->prepareSignId($inputJSON['merchant_order_id']);
if (!$bitpayX->verify($strToSign, $inputJSON['token'])) {
abort(500, 'sign error');
}
if ($params['status'] !== 'PAID') {
abort(500, 'order is not paid');
}
if (!$this->handle($params['merchant_order_id'], $params['order_id'])) {
abort(500, 'order process fail');
}
die('success');
}
public function payTaroNotify(Request $request)
{
Log::info('payTaroNotify: ' . json_encode($request->input()));
$payTaro = new PayTaro(config('v2board.paytaro_app_id'), config('v2board.paytaro_app_secret'));
if (!$payTaro->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) {
return true;
}
$order->status = 1;
$order->callback_no = $callbackNo;
return $order->save();
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Models\Order;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class PaymentController extends Controller
{
public function notify($method, $uuid, Request $request)
{
try {
$paymentService = new PaymentService($method, null, $uuid);
$verify = $paymentService->notify($request->input());
if (!$verify) abort(500, 'verify error');
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
abort(500, 'handle error');
}
die('success');
} catch (\Exception $e) {
abort(500, 'fail');
}
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if (!$order) {
abort(500, 'order is not found');
}
if ($order->status === 1) return true;
$orderService = new OrderService($order);
if (!$orderService->success($callbackNo)) {
return false;
}
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}

View File

@ -0,0 +1,199 @@
<?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']['text']) ? '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 || $user->is_staff) {
$ticketService->replyByAdmin(
$ticketId,
$msg->text,
$user->id
);
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
}
}

View File

@ -13,11 +13,20 @@ use App\Models\User;
use App\Models\InviteCode;
use App\Utils\Helper;
use App\Utils\Dict;
use App\Utils\CacheKey;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller
{
public function register(AuthRegister $request)
{
if ((int)config('v2board.recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, '验证码有误');
}
}
if ((int)config('v2board.email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
@ -26,6 +35,12 @@ class AuthController extends Controller
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, '本站已关闭注册');
}
@ -35,11 +50,10 @@ class AuthController extends Controller
}
}
if ((int)config('v2board.email_verify', 0)) {
$redisKey = 'sendEmailVerify:' . $request->input('email');
if (empty($request->input('email_code'))) {
abort(500, '邮箱验证码不能为空');
}
if (Cache::get($redisKey) !== $request->input('email_code')) {
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
}
}
@ -52,7 +66,7 @@ class AuthController extends Controller
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->v2ray_uuid = Helper::guid(true);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
@ -64,7 +78,7 @@ class AuthController extends Controller
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)config('v2board.invite_never_expire', env('V2BOARD_INVITE_NEVER_EXPIRE'))) {
if (!(int)config('v2board.invite_never_expire', 0)) {
$inviteCode->status = 1;
$inviteCode->save();
}
@ -86,7 +100,7 @@ class AuthController extends Controller
abort(500, '注册失败');
}
if ((int)config('v2board.email_verify', 0)) {
Cache::forget($redisKey);
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
@ -117,7 +131,8 @@ class AuthController extends Controller
}
$data = [
'token' => $user->token
'token' => $user->token,
'auth_data' => base64_encode("{$user->email}:{$user->password}")
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
@ -125,6 +140,10 @@ class AuthController extends Controller
$request->session()->put('is_admin', true);
$data['is_admin'] = true;
}
if ($user->is_staff) {
$request->session()->put('is_staff', true);
$data['is_staff'] = true;
}
return response([
'data' => $data
]);
@ -133,24 +152,17 @@ class AuthController extends Controller
public function token2Login(Request $request)
{
if ($request->input('token')) {
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
return header('Location:' . config('v2board.app_url'));
}
$code = Helper::guid();
$key = 'token2Login_' . $code;
Cache::put($key, $user->id, 600);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
$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);
return redirect()->to($location)->send();
}
if ($request->input('verify')) {
$key = 'token2Login_' . $request->input('verify');
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key);
if (!$userId) {
abort(500, '令牌有误');
@ -174,6 +186,45 @@ class AuthController extends Controller
}
}
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 getQuickLoginUrl(Request $request)
{
$authData = explode(':', base64_decode($request->input('auth_data')));
$user = User::where('email', $authData[0])
->where('password', $authData[1])
->first();
if (!$user) {
abort(500, '令牌有误');
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$url = config('v2board.app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
public function check(Request $request)
{
$data = [
@ -189,8 +240,7 @@ class AuthController extends Controller
public function forget(AuthForget $request)
{
$redisKey = 'sendEmailVerify:' . $request->input('email');
if (Cache::get($redisKey) !== $request->input('email_code')) {
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();
@ -202,7 +252,7 @@ class AuthController extends Controller
if (!$user->save()) {
abort(500, '重置失败');
}
Cache::forget($redisKey);
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return response([
'data' => true
]);

View File

@ -9,9 +9,11 @@ use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Mail;
use App\Utils\Helper;
use Illuminate\Support\Facades\Cache;
use App\Jobs\SendEmail;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\Dict;
use App\Utils\CacheKey;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
@ -23,7 +25,11 @@ class CommController extends Controller
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'emailWhitelistSuffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0
: 0,
'isRecaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptchaSiteKey' => config('v2board.recaptcha_site_key'),
'appDescription' => config('v2board.app_description'),
'appUrl' => config('v2board.app_url')
]
]);
}
@ -37,26 +43,33 @@ class CommController extends Controller
public function sendEmailVerify(CommSendEmailVerify $request)
{
$email = $request->input('email');
$cacheKey = 'sendEmailVerify:' . $email;
if (Cache::get($cacheKey)) {
abort(500, '验证码已发送,请过一会在请求');
if ((int)config('v2board.recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, '验证码有误');
}
$code = Helper::randomChar(6);
}
$email = $request->input('email');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
abort(500, '验证码已发送,请过一会再请求');
}
$code = rand(100000, 999999);
$subject = config('v2board.app_name', 'V2Board') . '邮箱验证码';
SendEmail::dispatch([
SendEmailJob::dispatch([
'email' => $email,
'subject' => $subject,
'template_name' => 'mail.sendEmailVerify',
'template_name' => 'verify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'code' => $code,
'url' => config('v2board.app_url')
]
])->onQueue('verify_mail');
]);
Cache::put($cacheKey, $code, 60);
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
]);

View File

@ -2,18 +2,24 @@
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\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
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":{}},"outboundDetour":[{"protocol":"blackhole","settings":{},"tag":"block"}],"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');
@ -33,32 +39,19 @@ class DeepbworkController extends Controller
if (!$server) {
abort(500, 'fail');
}
Cache::put('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@v2board.user", $user->v2ray_uuid),
"alter_id" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $server->alter_id,
"level" => 0,
];
unset($user['v2ray_uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
unset($user['uuid']);
unset($user['email']);
array_push($result, $user);
}
return response([
@ -70,33 +63,36 @@ class DeepbworkController extends Controller
// 后端提交数据
public function submit(Request $request)
{
Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
// 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'
'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);
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$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();
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
DB::commit();
return response([
'ret' => 1,
@ -112,65 +108,11 @@ class DeepbworkController extends Controller
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$server = Server::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$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 ($server->rules) {
$rules = json_decode($server->rules);
// domain
if (isset($rules->domain)) {
$domainObj = new \StdClass();
$domainObj->type = 'field';
$domainObj->domain = $rules->domain;
$domainObj->outboundTag = 'block';
array_push($json->routing->settings->rules, $domainObj);
}
// protocol
if (isset($rules->protocol)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $rules->protocol;
$protocolObj->outboundTag = 'block';
array_push($json->routing->settings->rules, $protocolObj);
}
}
if ((int)$server->tls) {
$json->inbound->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/home/v2ray.crt',
'keyFile' => '/home/v2ray.key'
];
$json->inbound->streamSettings->tlsSettings = new \StdClass();
$json->inbound->streamSettings->tlsSettings->certificates[0] = $tls;
$serverService = new ServerService();
try {
$json = $serverService->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));

View File

@ -2,6 +2,9 @@
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;
@ -11,9 +14,18 @@ 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
{
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 $poseidonVersion;
public function __construct(Request $request)
{
$this->poseidonVersion = $request->input('poseidon_version');
}
// 后端获取用户
public function user(Request $request)
@ -25,33 +37,19 @@ class PoseidonController extends Controller
if (!$server) {
return $this->error("server could not be found", 404);
}
Cache::put('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@v2board.user", $user->v2ray_uuid),
"alter_id" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $server->alter_id,
"level" => 0,
];
unset($user['v2ray_uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
unset($user['uuid']);
unset($user['email']);
array_push($result, $user);
}
@ -62,30 +60,21 @@ class PoseidonController extends Controller
public function submit(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
if (!$server) {
return $this->error("server could not be found", 404);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$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();
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess')) {
return $this->error("user fetch fail", 500);
}
}
return $this->success('');
@ -101,47 +90,32 @@ class PoseidonController extends Controller
if (empty($nodeId) || empty($localPort)) {
return $this->error('invalid parameters', 400);
}
$server = Server::find($nodeId);
if (!$server) {
return $this->error("server could not be found", 404);
}
$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;
}
$serverService = new ServerService();
try {
$json = $serverService->getV2RayConfig($nodeId, $localPort);
$json->poseidon = [
'license_key' => (string)config('v2board.server_license'),
];
if ($this->poseidonVersion >= 'v1.5.0') {
// don't need it after v1.5.0
unset($json->inboundDetour);
unset($json->stats);
unset($json->api);
array_shift($json->routing->rules);
}
foreach($json->policy->levels as &$level) {
$level->handshake = 2;
$level->uplinkOnly = 2;
$level->downlinkOnly = 2;
$level->connIdle = 60;
}
return $this->success($json);
} catch (\Exception $e) {
return $this->error($e->getMessage(), 500);
}
}
protected function verifyToken(Request $request)
@ -162,9 +136,23 @@ class PoseidonController extends Controller
}
protected function success($data) {
$req = request();
// Only for "GET" method
if (!$req->isMethod('GET') || !$data) {
return response([
'msg' => 'ok',
'data' => $data,
]);
}
$etag = sha1(json_encode($data));
if ($etag == $req->header("IF-NONE-MATCH")) {
return response(null, 304);
}
return response([
'msg' => 'ok',
'data' => $data,
])->header('ETAG', $etag);
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\Server;
use App\Models\ServerShadowsocks;
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\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Shadowsocks
* Github: https://github.com/tokumeikoi/tidalab-ss
*/
class ShadowsocksTidalabController 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 = ServerShadowsocks::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$result = [];
foreach ($users as $user) {
array_push($result, [
'id' => $user->id,
'port' => $server->server_port,
'cipher' => $server->cipher,
'secret' => $user->uuid
]);
}
return response([
'data' => $result
]);
}
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerShadowsocks::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_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch((float)$u, (float)$d, (int)$item['user_id'], $server, 'shadowsocks')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
DB::commit();
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
}

View File

@ -0,0 +1,117 @@
<?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['email']);
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);
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
try {
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'], $server, 'trojan')) {
continue;
}
}
} catch (\Exception $e) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
DB::commit();
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

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Staff;
use App\Http\Requests\Admin\NoticeSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Notice::orderBy('id', 'DESC')->get()
]);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
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 drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
if (!$notice->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Staff;
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)
{
$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' => $plans
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Staff;
use App\Services\TicketService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return response([
'data' => $ticket
]);
}
$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($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
}
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->session()->get('id')
);
return response([
'data' => true
]);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Staff;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Staff\UserUpdate;
use App\Jobs\SendEmailJob;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
class UserController extends Controller
{
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::where('is_admin', 0)
->where('id', $request->input('id'))
->where('is_staff', 0)
->first();
if (!$user) abort(500, '用户不存在');
return response([
'data' => $user
]);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
abort(500, '邮箱已被使用');
}
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($params['password']);
}
if (isset($params['plan_id'])) {
$plan = Plan::find($params['plan_id']);
if (!$plan) {
abort(500, '订阅计划不存在');
}
$params['group_id'] = $plan->group_id;
}
try {
$user->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sendMail(UserSendMail $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
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
]);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\User;
use App\Models\Payment;
use App\Utils\Dict;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'is_telegram' => (int)config('v2board.telegram_bot_enable', 0),
'stripe_pk' => config('v2board.stripe_pk_live'),
'withdraw_methods' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0)
]
]);
}
public function getStripePublicKey(Request $request)
{
$payment = Payment::where('id', $request->input('id'))
->where('payment', 'StripeCredit')
->first();
if (!$payment) abort(500, 'payment is not found');
$config = json_decode($payment->config, true);
return response([
'data' => $config['stripe_pk_live']
]);
}
}

View File

@ -11,20 +11,26 @@ class CouponController extends Controller
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, '优惠券码不能为空');
abort(500, __('user.coupon.check.coupon_not_empty'));
}
$coupon = Coupon::where('code', $request->input('code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
abort(500, __('user.coupon.check.coupon_invalid'));
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
abort(500, __('user.coupon.check.coupon_not_available_by_number'));
}
if (time() < $coupon->started_at) {
abort(500, '优惠券还未到可用时间');
abort(500, __('user.coupon.check.coupon_not_available_by_time'));
}
if (time() > $coupon->ended_at) {
abort(500, '优惠券已过期');
abort(500, __('user.coupon.check.coupon_expired'));
}
if ($coupon->limit_plan_ids) {
$limitPlanIds = json_decode($coupon->limit_plan_ids);
if (!in_array($request->input('plan_id'), $limitPlanIds)) {
abort(500, __('user.coupon.check.coupon_limit_plan'));
}
}
return response([
'data' => $coupon

View File

@ -14,7 +14,7 @@ class InviteController extends Controller
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, '已达到创建数量上限');
abort(500, __('user.invite.save.invite_create_limit'));
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->session()->get('id');
@ -28,6 +28,7 @@ class InviteController extends Controller
{
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('commission_balance', '>', 0)
->where('status', 3)
->select([
'id',
@ -45,7 +46,7 @@ class InviteController extends Controller
$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;

View File

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Http\Request;
use App\Models\Knowledge;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::where('id', $request->input('id'))
->where('show', 1)
->first()
->toArray();
if (!$knowledge) abort(500, __('user.knowledge.fetch.knowledge_not_exist'));
$user = User::find($request->session()->get('id'));
$userService = new UserService();
if ($userService->isAvailable($user)) {
$appleId = config('v2board.apple_id');
$appleIdPassword = config('v2board.apple_id_password');
} else {
$appleId = __('user.knowledge.fetch.apple_id_must_be_plan');
$appleIdPassword = __('user.knowledge.fetch.apple_id_must_be_plan');
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{appleId}}', $appleId, $knowledge['body']);
$knowledge['body'] = str_replace('{{appleIdPassword}}', $appleIdPassword, $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
$knowledge['body'] = str_replace(
'{{safeBase64SubscribeUrl}}',
str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($subscribeUrl)
),
$knowledge['body']
);
return response([
'data' => $knowledge
]);
}
$knowledges = Knowledge::select(['id', 'category', 'title', 'updated_at'])
->where('language', $request->input('language'))
->where('show', 1)
->orderBy('sort', 'ASC')
->get()
->groupBy('category');
return response([
'data' => $knowledges
]);
}
private function formatAccessData(&$body)
{
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $substr;}
$accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
if ($accessData) {
$body = str_replace($accessData, '<div class="v2board-no-access">'. __('user.knowledge.formatAccessData.no_access') .'</div>', $body);
}
}
}

View File

@ -4,20 +4,24 @@ namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Models\Payment;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Models\Coupon;
use App\Utils\Helper;
use Omnipay\Omnipay;
use Stripe\Stripe;
use Stripe\Source;
use Library\BitpayX;
use Library\PayTaro;
use Library\MGate;
use Library\Epay;
class OrderController extends Controller
{
@ -38,7 +42,7 @@ class OrderController extends Controller
}
}
return response([
'data' => $order
'data' => $order->makeHidden(['id', 'user_id'])
]);
}
@ -48,132 +52,101 @@ class OrderController extends Controller
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('user.order.details.order_not_exist'));
}
$order['plan'] = Plan::find($order->plan_id);
$order['plan_transfer_hour'] = config('v2board.plan_transfer_hour', 12);
$order['try_out_plan_id'] = (int)config('v2board.try_out_plan_id');
if (!$order['plan']) {
abort(500, '订阅不存在');
abort(500, __('user.order.details.plan_not_exist'));
}
return response([
'data' => $order
]);
}
private function isNotCompleteOrderByUserId($userId)
{
$order = Order::whereIn('status', [0, 1])
->where('user_id', $userId)
->first();
if (!$order) {
return false;
}
return true;
}
public function save(OrderSave $request)
{
if ($this->isNotCompleteOrderByUserId($request->session()->get('id'))) {
abort(500, '存在未付款订单,请取消后再试');
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->session()->get('id'))) {
abort(500, __('user.order.save.exist_open_order'));
}
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
abort(500, __('user.order.save.plan_not_exist'));
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
abort(500, '该订阅已售罄');
if ($request->input('cycle') !== 'reset_price') {
abort(500, __('user.order.save.plan_stop_sell'));
}
}
if (!$plan->renew && $user->plan_id == $plan->id) {
abort(500, '该订阅无法续费,请更换其他订阅');
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('cycle') !== 'reset_price') {
abort(500, __('user.order.save.plan_stop_renew'));
}
if ($plan[$request->input('cycle')] === NULL) {
abort(500, '该订阅周期无法进行购买,请选择其他周期');
abort(500, __('user.order.save.plan_stop'));
}
if ($request->input('coupon_code')) {
$coupon = Coupon::where('code', $request->input('coupon_code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
if ($request->input('cycle') === 'reset_price') {
if ($user->expired_at <= time() || !$user->plan_id) {
abort(500, __('user.order.save.plan_exist_not_buy_package'));
}
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 (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('user.order.save.plan_expired'));
}
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')];
// renew and change subscribe process
if ($user->expired_at > time() && $order->plan_id !== $user->plan_id) {
$order->type = 3;
if (!(int)config('v2board.plan_change_enable', 1)) abort(500, '目前不允许更改订阅,请联系管理员');
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2;
} else {
$order->type = 1;
}
// discount start
// coupon
if (isset($coupon)) {
switch ($coupon->type) {
case 1:
$order->discount_amount = $coupon->value;
break;
case 2:
$order->discount_amount = $order->total_amount * ($coupon->value / 100);
break;
}
if ($coupon->limit_use !== NULL) {
$coupon->limit_use = $coupon->limit_use - 1;
if (!$coupon->save()) {
DB::rollback();
abort(500, '优惠券使用失败');
}
}
}
// user
if ($user->discount) {
$order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100));
}
// discount complete
$order->total_amount = $order->total_amount - $order->discount_amount;
// discount end
// free process
if ($order->total_amount <= 0) {
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, __('user.order.save.coupon_use_failed'));
}
$order->coupon_id = $couponService->getId();
}
$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, __('user.order.save.insufficient_balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
$order->status = 1;
}
// invite process
if ($user->invite_user_id && $order->total_amount > 0) {
$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 (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, __('user.order.save.insufficient_balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
abort(500, __('user.order.save.order_create_failed'));
}
DB::commit();
@ -192,57 +165,32 @@ class OrderController extends Controller
->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, '支付方式不可用');
abort(500, __('user.order.checkout.order_not_exist_or_paid'));
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
return response([
'type' => 0,
'data' => $this->alipayF2F($tradeNo, $order->total_amount)
'type' => -1,
'data' => true
]);
case 2:
// stripeAlipay
if (!(int)config('v2board.stripe_alipay_enable')) {
abort(500, '支付方式不可用');
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('user.order.checkout.pay_method_not_use'));
$paymentService = new PaymentService($payment->payment, $payment->id);
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => $order->total_amount,
'user_id' => $order->user_id,
'stripe_token' => $request->input('token')
]);
$order->update(['payment_id' => $method]);
return response([
'type' => 1,
'data' => $this->stripeAlipay($order)
'type' => $result['type'],
'data' => $result['data']
]);
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.paytaro_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->payTaro($order)
]);
default:
abort(500, '支付方式不存在');
}
}
public function check(Request $request)
@ -252,7 +200,7 @@ class OrderController extends Controller
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('user.order.check.order_not_exist'));
}
return response([
'data' => $order->status
@ -261,178 +209,38 @@ class OrderController extends Controller
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 = 'wallet';
array_push($data, $bitpayX);
}
if ((int)config('v2board.paytaro_enable')) {
$obj = new \StdClass();
$obj->name = '聚合支付';
$obj->method = 5;
$obj->icon = 'wallet';
array_push($data, $obj);
}
$methods = Payment::select([
'id',
'name',
'payment'
])
->where('enable', 1)->get();
return response([
'data' => $data
'data' => $methods
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数有误');
abort(500, __('user.order.cancel.params_wrong'));
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('user.order.cancel.order_not_exist'));
}
if ($order->status !== 0) {
abort(500, '只可以取消待支付订单');
abort(500, __('user.order.cancel.only_cancel_pending_order'));
}
$order->status = 2;
if (!$order->save()) {
abort(500, '取消失败');
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, __('user.order.cancel.cancel_failed'));
}
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)
{
$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 (!Cache::put($source['id'], $order->trade_no, 3600)) {
abort(500, '订单创建失败');
}
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 (!Cache::put($source['id'], $order->trade_no, 3600)) {
abort(500, '订单创建失败');
}
return $source['wechat']['qr_code_url'];
}
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 payTaro($order)
{
$payTaro = new PayTaro(config('v2board.paytaro_app_id'), config('v2board.paytaro_app_secret'));
$result = $payTaro->pay([
'app_id' => config('v2board.paytaro_app_id'),
'out_trade_no' => $order->trade_no,
'total_amount' => $order->total_amount,
'notify_url' => url('/api/v1/guest/order/payTaroNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]);
return $result;
}
}

View File

@ -14,13 +14,14 @@ class PlanController extends Controller
$plan = Plan::where('id', $request->input('id'))
->first();
if (!$plan) {
abort(500, '该订阅不存在');
abort(500, __('user.plan.fetch.plan_not_exist'));
}
return response([
'data' => $plan
]);
}
$plan = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
return response([
'data' => $plan

View File

@ -3,6 +3,9 @@
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;
@ -16,28 +19,14 @@ 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);
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get('server_last_check_at_' . $server[$i]['parent_id']);
} else {
$server[$i]['last_check_at'] = Cache::get('server_last_check_at_' . $server[$i]['id']);
}
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
}
return response([
'data' => $server
'data' => $servers
]);
}
@ -58,17 +47,12 @@ class ServerController extends Controller
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
'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

@ -4,10 +4,14 @@ namespace App\Http\Controllers\User;
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 App\Utils\Dict;
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
@ -19,7 +23,7 @@ class TicketController extends Controller
->where('user_id', $request->session()->get('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('user.ticket.fetch.ticket_not_exist'));
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
@ -51,6 +55,9 @@ class TicketController extends Controller
public function save(TicketSave $request)
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
abort(500, __('user.ticket.save.exist_other_open_ticket'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
@ -60,7 +67,7 @@ class TicketController extends Controller
]));
if (!$ticket) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('user.ticket.save.ticket_create_failed'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
@ -69,9 +76,10 @@ class TicketController extends Controller
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('user.ticket.save.ticket_create_failed'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
return response([
'data' => true
]);
@ -80,22 +88,22 @@ class TicketController extends Controller
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
abort(500, __('user.ticket.reply.params_wrong'));
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
abort(500, __('user.ticket.reply.message_not_empty'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('user.ticket.reply.ticket_not_exist'));
}
if ($ticket->status) {
abort(500, '工单已关闭,无法回复');
abort(500, __('user.ticket.reply.ticket_close_not_reply'));
}
if ($request->session()->get('id') == $this->getLastMessage($ticket->id)->user_id) {
abort(500, '请等待技术支持回复');
abort(500, __('user.ticket.reply.wait_reply'));
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
@ -106,9 +114,10 @@ class TicketController extends Controller
$ticket->last_reply_user_id = $request->session()->get('id');
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
abort(500, __('user.ticket.reply.ticket_reply_failed'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
return response([
'data' => true
]);
@ -118,17 +127,17 @@ class TicketController extends Controller
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
abort(500, __('user.ticket.close.params_wrong'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('user.ticket.close.ticket_not_exist'));
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
abort(500, __('user.ticket.close.close_failed'));
}
return response([
'data' => true
@ -141,4 +150,61 @@ class TicketController extends Controller
->orderBy('id', 'DESC')
->first();
}
public function withdraw(TicketWithdraw $request)
{
if ((int)config('v2board.withdraw_close_enable', 0)) {
abort(500, 'user.ticket.withdraw.not_support_withdraw');
}
if (!in_array(
$request->input('withdraw_method'),
config(
'v2board.commission_withdraw_method',
Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT
)
)) {
abort(500, __('user.ticket.withdraw.not_support_withdraw_method'));
}
$user = User::find($request->session()->get('id'));
$limit = config('v2board.commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
abort(500, __('user.ticket.withdraw.system_require_withdraw_limit', ['limit' => $limit]));
}
DB::beginTransaction();
$subject = __('user.ticket.withdraw.ticket_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, __('user.ticket.withdraw.ticket_create_failed'));
}
$message = __('user.ticket.withdraw.ticket_message', [
'method' => $request->input('withdraw_method'),
'account' => $request->input('withdraw_account')
]);
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, __('user.ticket.withdraw.ticket_create_failed'));
}
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}`", true);
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Tutorial;
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', 'title', 'description', 'icon'])
->where('show', 1)
->get();
$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() ? config('v2board.apple_id', '管理员暂无提供AppleID信息') : '账号过期或未订阅',
'apple_id_password' => $user->expired_at > time() ? 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
return response($response);
}
}

View File

@ -3,7 +3,9 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
@ -23,26 +25,23 @@ class UserController extends Controller
]);
}
public function changePassword(Request $request)
public function changePassword(UserChangePassword $request)
{
if (empty($request->input('old_password'))) {
abort(500, '旧密码不能为空');
}
if (empty($request->input('new_password'))) {
abort(500, '新密码不能为空');
}
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('user.user.changePassword.user_not_exist'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$request->input('old_password'),
$user->password)
) {
abort(500, '旧密码有误');
abort(500, __('user.user.changePassword.old_password_wrong'));
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
if (!$user->save()) {
abort(500, '保存失败');
abort(500, __('user.user.changePassword.save_failed'));
}
$request->session()->flush();
return response([
@ -58,8 +57,7 @@ class UserController extends Controller
'transfer_enable',
'last_login_at',
'created_at',
'enable',
'is_admin',
'banned',
'remind_expire',
'remind_traffic',
'expired_at',
@ -67,9 +65,13 @@ class UserController extends Controller
'commission_balance',
'plan_id',
'discount',
'commission_rate'
'commission_rate',
'telegram_id'
])
->first();
if (!$user) {
abort(500, __('user.user.info.user_not_exist'));
}
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return response([
'data' => $user
@ -95,14 +97,29 @@ class UserController extends Controller
public function getSubscribe(Request $request)
{
$user = User::find($request->session()->get('id'));
$user = User::where('id', $request->session()->get('id'))
->select([
'id',
'plan_id',
'token',
'expired_at',
'u',
'd',
'transfer_enable',
'email'
])
->first();
if (!$user) {
abort(500, __('user.user.getSubscribe.user_not_exist'));
}
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, '订阅计划不存在');
abort(500, __('user.user.getSubscribe.plan_not_exist'));
}
}
$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
]);
@ -111,10 +128,13 @@ class UserController extends Controller
public function resetSecurity(Request $request)
{
$user = User::find($request->session()->get('id'));
$user->v2ray_uuid = Helper::guid(true);
if (!$user) {
abort(500, __('user.user.resetSecurity.user_not_exist'));
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, '重置失败');
abort(500, __('user.user.resetSecurity.reset_failed'));
}
return response([
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
@ -130,14 +150,58 @@ class UserController extends Controller
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
abort(500, __('user.user.update.user_not_exist'));
}
if (!$user->update($updateData)) {
abort(500, '保存失败');
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, __('user.user.update.save_failed'));
}
return response([
'data' => true
]);
}
public function transfer(UserTransfer $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('user.user.transfer.user_not_exist'));
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, __('user.user.transfer.insufficient_commission_balance'));
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
abort(500, __('user.user.transfer.transfer_failed'));
}
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 && (int)$day >= (int)$lastDay) {
return $lastDay - $today;
}
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',
\App\Http\Middleware\Language::class,
'bindings',
],
];
@ -68,7 +68,7 @@ class Kernel extends HttpKernel
'user' => \App\Http\Middleware\User::class,
'admin' => \App\Http\Middleware\Admin::class,
'client' => \App\Http\Middleware\Client::class,
'server' => \App\Http\Middleware\Server::class,
'staff' => \App\Http\Middleware\Staff::class,
];
/**

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
class Language
{
public function handle($request, Closure $next)
{
if ($request->header('content-language')) {
App::setLocale($request->header('content-language'));
}
return $next($request);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
class Staff
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (!$request->session()->get('is_staff')) {
abort(403, '权限不足');
}
return $next($request);
}
}

View File

@ -15,13 +15,22 @@ class User
*/
public function handle($request, Closure $next)
{
if ($request->input('access_token')) {
$user = \App\Models\User::where('token', $request->input('access_token'))->first();
if ($request->input('auth_data')) {
$authData = explode(':', base64_decode($request->input('auth_data')));
$user = \App\Models\User::where('password', $authData[1])
->where('email', $authData[0])
->first();
if ($user) {
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
}
// if ($request->input('lang')) {
// $request->session()->put('lang', $request->input('lang'));
// }
// if ($request->session()->get('lang')) {
// App::setLocale($request->session()->get('lang'));
// }
if (!$request->session()->get('id')) {
abort(403, '未登录或登陆已过期');
}

View File

@ -6,28 +6,53 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest
{
CONST RULES = [
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
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',
'commission_withdraw_limit' => 'nullable|numeric',
'commission_withdraw_method' => 'nullable|array',
'withdraw_close_enable' => 'in:0,1',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'url',
'subscribe_url' => 'url',
'plan_transfer_hour' => 'numeric',
'plan_change_enable' => 'in:0,1',
'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_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'tos_url' => 'nullable|url',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'renew_reset_traffic_enable' => 'in:0,1',
'surplus_enable' => 'in:0,1',
// server
'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' => 'nullable|integer|min:16',
@ -36,42 +61,69 @@ class ConfigSave extends FormRequest
// 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' => '',
// paytaro
'paytaro_enable' => 'in:0,1',
'paytaro_app_id' => '',
'paytaro_app_secret' => '',
// 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' => 'in:1,2',
'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' => '',
'frontend_customer_service_method' => '',
'frontend_customer_service_id' => '',
// tutorial
'apple_id' => 'email',
'apple_id_password' => ''
'apple_id' => 'nullable|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' => '',
// app
'windows_version' => '',
'windows_download_url' => '',
'macos_version' => '',
'macos_download_url' => '',
'android_version' => '',
'android_download_url' => ''
];
public static function filter()
{
return array_keys(self::RULES);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
}
public function messages()
{
// illiteracy prompt
return [
'app_url.url' => '站点URL格式不正确必须携带http(s)://',
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://',
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确'
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class CouponGenerate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'generate_count' => 'nullable|integer|max:500',
'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 [
'generate_count.integer' => '生成数量必须为数字',
'generate_count.max' => '生成数量最大为500个',
'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

@ -19,7 +19,9 @@ class CouponSave extends FormRequest
'value' => 'required|integer',
'started_at' => 'required|integer',
'ended_at' => 'required|integer',
'limit_use' => 'nullable|integer'
'limit_use' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'code' => ''
];
}
@ -35,7 +37,8 @@ class CouponSave extends FormRequest
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误'
'limit_use.integer' => '使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误'
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class KnowledgeCategorySave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'language' => 'required'
];
}
public function messages()
{
return [
'name.required' => '分类名称不能为空',
'language.required' => '分类语言不能为空'
];
}
}

View File

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

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class TutorialSave extends FormRequest
class KnowledgeSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,9 +14,10 @@ class TutorialSave extends FormRequest
public function rules()
{
return [
'category' => 'required',
'language' => 'required',
'title' => 'required',
'description' => 'required',
'icon' => 'required'
'body' => 'required'
];
}
@ -24,8 +25,9 @@ class TutorialSave extends FormRequest
{
return [
'title.required' => '标题不能为空',
'description.required' => '描述不能为空',
'icon.required' => '图标不能为空'
'category.required' => '分类不能为空',
'body.required' => '内容不能为空',
'language.required' => '语言不能为空'
];
}
}

View File

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

View File

@ -16,7 +16,7 @@ class NoticeSave extends FormRequest
return [
'title' => 'required',
'content' => 'required',
'img_url' => 'url'
'img_url' => 'nullable|url'
];
}

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,two_year_price,three_year_price,onetime_price,reset_price'
];
}
public function messages()
{
return [
'plan_id.required' => '订阅不能为空',
'email.required' => '邮箱不能为空',
'total_amount.required' => '支付金额不能为空',
'cycle.required' => '订阅周期不能为空',
'cycle.in' => '订阅周期格式有误'
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class OrderFetch extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'filter.*.key' => 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => ''
];
}
public function messages()
{
return [
'filter.*.key.required' => '过滤键不能为空',
'filter.*.key.in' => '过滤键参数有误',
'filter.*.condition.required' => '过滤条件不能为空',
'filter.*.condition.in' => '过滤条件参数有误',
];
}
}

View File

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

View File

@ -15,12 +15,17 @@ class PlanSave extends FormRequest
{
return [
'name' => 'required',
'content' => '',
'group_id' => 'required',
'transfer_enable' => 'required',
'month_price' => 'nullable|integer',
'quarter_price' => 'nullable|integer',
'half_year_price' => 'nullable|integer',
'year_price' => 'nullable|integer'
'year_price' => 'nullable|integer',
'two_year_price' => 'nullable|integer',
'three_year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer'
];
}
@ -28,12 +33,18 @@ class PlanSave extends FormRequest
{
return [
'name.required' => '套餐名称不能为空',
'type.required' => '套餐类型不能为空',
'type.in' => '套餐类型格式有误',
'group_id.required' => '权限组不能为空',
'transfer_enable.required' => '流量不能为空',
'month_price.integer' => '月付金额格式有误',
'quarter_price.integer' => '季付金额格式有误',
'half_year_price.integer' => '半年付金额格式有误',
'year_price.integer' => '年付金额格式有误'
'year_price.integer' => '年付金额格式有误',
'two_year_price.integer' => '两年付金额格式有误',
'three_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

@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerShadowsocksSave 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',
'cipher' => 'required|in:aes-128-gcm,aes-256-gcm,chacha20-ietf-poly1305',
'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' => '后端服务端口不能为空',
'cipher.required' => '加密方式不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerShadowsocksUpdate 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,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 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 [
'show.in' => '显示状态格式不正确'
];
}
}

View File

@ -4,10 +4,16 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends FormRequest
class ServerV2raySave extends FormRequest
{
CONST RULES = [
'rules' => '',
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
@ -16,19 +22,15 @@ class ServerSave extends FormRequest
'port' => 'required',
'server_port' => 'required',
'tls' => 'required',
'tags' => 'array',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'alter_id' => 'required|integer',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic',
'settings' => ''
'networkSettings' => '',
'ruleSettings' => '',
'tlsSettings' => '',
'dnsSettings' => ''
];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
}
public function messages()

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerUpdate extends FormRequest
class ServerV2rayUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UserFetch extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'filter.*.key' => 'required|in:id,email,transfer_enable,d,expired_at,uuid,token,invite_by_email,invite_user_id,plan_id,banned',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => 'required'
];
}
public function messages()
{
return [
'filter.*.key.required' => '过滤键不能为空',
'filter.*.key.in' => '过滤键参数有误',
'filter.*.condition.required' => '过滤条件不能为空',
'filter.*.condition.in' => '过滤条件参数有误',
'filter.*.value.required' => '过滤值不能为空'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UserGenerate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'generate_count' => 'nullable|integer|max:500',
'expired_at' => 'nullable|integer',
'plan_id' => 'nullable|integer',
'email_prefix' => 'nullable',
'email_suffix' => 'required',
'password' => 'nullable'
];
}
public function messages()
{
return [
'generate_count.integer' => '生成数量必须为数字',
'generate_count.max' => '生成数量最大为500个'
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UserSendMail extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'subject' => 'required',
'content' => 'required',
];
}
public function messages()
{
return [
'subject.required' => '主题不能为空',
'content.required' => '发送内容不能为空'
];
}
}

View File

@ -15,13 +15,20 @@ class UserUpdate extends FormRequest
{
return [
'email' => 'required|email',
'password' => 'nullable|min:8',
'transfer_enable' => 'numeric',
'expired_at' => 'integer',
'expired_at' => 'nullable|integer',
'banned' => 'required|in:0,1',
'is_admin' => 'required|in:0,1',
'plan_id' => 'integer',
'plan_id' => 'nullable|integer',
'commission_rate' => 'nullable|integer|min:0|max:100',
'discount' => 'nullable|integer|min:0|max:100'
'discount' => 'nullable|integer|min:0|max:100',
'is_admin' => 'required|in:0,1',
'is_staff' => 'required|in:0,1',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer',
'remarks' => 'nullable'
];
}
@ -36,6 +43,8 @@ class UserUpdate extends FormRequest
'banned.in' => '是否封禁格式不正确',
'is_admin.required' => '是否管理员不能为空',
'is_admin.in' => '是否管理员格式不正确',
'is_staff.required' => '是否员工不能为空',
'is_staff.in' => '是否员工格式不正确',
'plan_id.integer' => '订阅计划格式不正确',
'commission_rate.integer' => '推荐返利比例格式不正确',
'commission_rate.nullable' => '推荐返利比例格式不正确',
@ -44,7 +53,12 @@ class UserUpdate extends FormRequest
'discount.integer' => '专属折扣比例格式不正确',
'discount.nullable' => '专属折扣比例格式不正确',
'discount.min' => '专属折扣比例最小为0',
'discount.max' => '专属折扣比例最大为100'
'discount.max' => '专属折扣比例最大为100',
'u.integer' => '上行流量格式不正确',
'd.integer' => '下行流量格式不正确',
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确',
'password.min' => '密码长度最小8位'
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests\Staff;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email',
'password' => 'nullable',
'transfer_enable' => 'numeric',
'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',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'transfer_enable.numeric' => '流量格式不正确',
'expired_at.integer' => '到期时间格式不正确',
'banned.required' => '是否封禁不能为空',
'banned.in' => '是否封禁格式不正确',
'plan_id.integer' => '订阅计划格式不正确',
'commission_rate.integer' => '推荐返利比例格式不正确',
'commission_rate.nullable' => '推荐返利比例格式不正确',
'commission_rate.min' => '推荐返利比例最小为0',
'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

@ -15,7 +15,7 @@ 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,two_year_price,three_year_price,onetime_price,reset_price'
];
}

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',
'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

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class UserTransfer extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'transfer_amount' => 'required|integer|min:1'
];
}
public function messages()
{
return [
'transfer_amount.required' => '划转金额不能为空',
'transfer_amount.integer' => __('user.user.transfer.params_wrong'),
'transfer_amount.min' => __('user.user.transfer.params_wrong')
];
}
}

View File

@ -14,29 +14,71 @@ class AdminRoute
// 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/fetch', 'Admin\\ServerController@fetch');
$router->post('/server/save', 'Admin\\ServerController@save');
$router->get ('/server/group/fetch', 'Admin\\ServerController@groupFetch');
$router->post('/server/group/save', 'Admin\\ServerController@groupSave');
$router->post('/server/group/drop', 'Admin\\ServerController@groupDrop');
$router->post('/server/drop', 'Admin\\ServerController@drop');
$router->post('/server/update', 'Admin\\ServerController@update');
$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->get ('/server/manage/getNodes', 'Admin\\Server\\ManageController@getNodes');
$router->post('/server/manage/sort', 'Admin\\Server\\ManageController@sort');
$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');
});
$router->group([
'prefix' => 'server/shadowsocks'
], function ($router) {
$router->get ('fetch', 'Admin\\Server\\ShadowsocksController@fetch');
$router->post('save', 'Admin\\Server\\ShadowsocksController@save');
$router->post('drop', 'Admin\\Server\\ShadowsocksController@drop');
$router->post('update', 'Admin\\Server\\ShadowsocksController@update');
$router->post('copy', 'Admin\\Server\\ShadowsocksController@copy');
$router->post('sort', 'Admin\\Server\\ShadowsocksController@sort');
});
// 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->post('/user/generate', 'Admin\\UserController@generate');
$router->post('/user/dumpCSV', 'Admin\\UserController@dumpCSV');
$router->post('/user/sendMail', 'Admin\\UserController@sendMail');
$router->post('/user/ban', 'Admin\\UserController@ban');
$router->post('/user/resetSecret', 'Admin\\UserController@resetSecret');
$router->post('/user/setInviteUser', 'Admin\\UserController@setInviteUser');
// StatOrder
$router->get ('/stat/getOverride', 'Admin\\StatController@getOverride');
$router->get ('/stat/getServerLastRank', 'Admin\\StatController@getServerLastRank');
$router->get ('/stat/getOrder', 'Admin\\StatController@getOrder');
// Notice
$router->get ('/notice/fetch', 'Admin\\NoticeController@fetch');
$router->post('/notice/save', 'Admin\\NoticeController@save');
@ -46,17 +88,23 @@ class AdminRoute
$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/generate', 'Admin\\CouponController@generate');
$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');
// Knowledge
$router->get ('/knowledge/fetch', 'Admin\\KnowledgeController@fetch');
$router->get ('/knowledge/getCategory', 'Admin\\KnowledgeController@getCategory');
$router->post('/knowledge/save', 'Admin\\KnowledgeController@save');
$router->post('/knowledge/show', 'Admin\\KnowledgeController@show');
$router->post('/knowledge/drop', 'Admin\\KnowledgeController@drop');
$router->post('/knowledge/sort', 'Admin\\KnowledgeController@sort');
// Payment
$router->get ('/payment/fetch', 'Admin\\PaymentController@fetch');
$router->get ('/payment/getPaymentMethods', 'Admin\\PaymentController@getPaymentMethods');
$router->post('/payment/getPaymentForm', 'Admin\\PaymentController@getPaymentForm');
$router->post('/payment/save', 'Admin\\PaymentController@save');
$router->post('/payment/drop', 'Admin\\PaymentController@drop');
});
}
}

View File

@ -14,8 +14,9 @@ class ClientRoute
// Client
$router->get('/subscribe', 'Client\\ClientController@subscribe');
// App
$router->get('/app/data', 'Client\\AppController@data');
$router->get('/app/config', 'Client\\AppController@config');
$router->get('/app/getConfig', 'Client\\AppController@getConfig');
$router->get('/app/getVersion', 'Client\\AppController@getVersion');
});
}
}

View File

@ -12,11 +12,12 @@ class GuestRoute
], 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/payTaroNotify', 'Guest\\OrderController@payTaroNotify');
// Telegram
$router->post('/telegram/webhook', 'Guest\\TelegramController@webhook');
// Payment
$router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', 'Guest\\PaymentController@notify');
// Comm
$router->get ('/comm/config', 'Guest\\CommController@config');
});
}
}

View File

@ -10,17 +10,18 @@ class PassportRoute
$router->group([
'prefix' => 'passport'
], function ($router) {
// TODO: 1.1.1 abolish
$router->post('/login', 'Passport\\AuthController@login');
// Auth
$router->post('/auth/register', 'Passport\\AuthController@register');
$router->post('/auth/login', 'Passport\\AuthController@login');
$router->get ('/auth/token2Login', 'Passport\\AuthController@token2Login');
$router->get ('/auth/check', 'Passport\\AuthController@check');
$router->post('/auth/forget', 'Passport\\AuthController@forget');
$router->post('/auth/getTempToken', 'Passport\\AuthController@getTempToken');
$router->post('/auth/getQuickLoginUrl', 'Passport\\AuthController@getQuickLoginUrl');
// Comm
$router->get ('/comm/config', 'Passport\\CommController@config');
$router->post('/comm/sendEmailVerify', 'Passport\\CommController@sendEmailVerify');
$router->post('/comm/pv', 'Passport\\CommController@pv');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Routes;
use Illuminate\Contracts\Routing\Registrar;
class StaffRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'staff',
'middleware' => 'staff'
], function ($router) {
// Ticket
$router->get ('/ticket/fetch', 'Staff\\TicketController@fetch');
$router->post('/ticket/reply', 'Staff\\TicketController@reply');
$router->post('/ticket/close', 'Staff\\TicketController@close');
// User
$router->post('/user/update', 'Staff\\UserController@update');
$router->get ('/user/getUserInfoById', 'Staff\\UserController@getUserInfoById');
$router->post('/user/sendMail', 'Staff\\UserController@sendMail');
$router->post('/user/ban', 'Staff\\UserController@ban');
// Plan
$router->get ('/plan/fetch', 'Staff\\PlanController@fetch');
// 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');
});
}
}

View File

@ -19,6 +19,7 @@ class UserRoute
$router->post('/update', 'User\\UserController@update');
$router->get ('/getSubscribe', 'User\\UserController@getSubscribe');
$router->get ('/getStat', 'User\\UserController@getStat');
$router->post('/transfer', 'User\\UserController@transfer');
// Order
$router->post('/order/save', 'User\\OrderController@save');
$router->post('/order/checkout', 'User\\OrderController@checkout');
@ -33,10 +34,6 @@ class UserRoute
$router->get ('/invite/save', 'User\\InviteController@save');
$router->get ('/invite/fetch', 'User\\InviteController@fetch');
$router->get ('/invite/details', 'User\\InviteController@details');
// Tutorial
$router->get ('/tutorial/getSubscribeUrl', 'User\\TutorialController@getSubscribeUrl');
$router->get ('/tutorial/getAppleID', 'User\\TutorialController@getAppleID');
$router->get ('/tutorial/fetch', 'User\\TutorialController@fetch');
// Notice
$router->get ('/notice/fetch', 'User\\NoticeController@fetch');
// Ticket
@ -44,11 +41,20 @@ class UserRoute
$router->post('/ticket/close', 'User\\TicketController@close');
$router->post('/ticket/save', 'User\\TicketController@save');
$router->get ('/ticket/fetch', 'User\\TicketController@fetch');
$router->post('/ticket/withdraw', 'User\\TicketController@withdraw');
// Server
$router->get ('/server/fetch', 'User\\ServerController@fetch');
$router->get ('/server/log/fetch', 'User\\ServerController@logFetch');
// Coupon
$router->post('/coupon/check', 'User\\CouponController@check');
// Telegram
$router->get ('/telegram/getBotInfo', 'User\\TelegramController@getBotInfo');
// Comm
$router->get ('/comm/config', 'User\\CommController@config');
$router->Post('/comm/getStripePublicKey', 'User\\CommController@getStripePublicKey');
// Knowledge
$router->get ('/knowledge/fetch', 'User\\KnowledgeController@fetch');
$router->get ('/knowledge/getCategory', 'User\\KnowledgeController@getCategory');
});
}
}

View File

@ -7,10 +7,11 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use App\Models\MailLog;
class SendEmail implements ShouldQueue
class SendEmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $params;
@ -22,6 +23,8 @@ class SendEmail implements ShouldQueue
*/
public function __construct($params)
{
$this->delay(now()->addSecond(2));
$this->onQueue('send_email');
$this->params = $params;
}
@ -32,9 +35,19 @@ class SendEmail implements ShouldQueue
*/
public function handle()
{
if (config('v2board.email_host')) {
Config::set('mail.host', config('v2board.email_host', env('mail.host')));
Config::set('mail.port', config('v2board.email_port', env('mail.port')));
Config::set('mail.encryption', config('v2board.email_encryption', env('mail.encryption')));
Config::set('mail.username', config('v2board.email_username', env('mail.username')));
Config::set('mail.password', config('v2board.email_password', env('mail.password')));
Config::set('mail.from.address', config('v2board.email_from_address', env('mail.from.address')));
Config::set('mail.from.name', config('v2board.app_name', 'V2Board'));
}
$params = $this->params;
$email = $params['email'];
$subject = $params['subject'];
$params['template_name'] = 'mail.' . config('v2board.email_template', 'default') . '.' . $params['template_name'];
try {
Mail::send(
$params['template_name'],

View File

@ -0,0 +1,43 @@
<?php
namespace App\Jobs;
use App\Services\TelegramService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendTelegramJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $telegramId;
protected $text;
public $tries = 3;
public $timeout = 5;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(int $telegramId, string $text)
{
$this->onQueue('send_telegram');
$this->telegramId = $telegramId;
$this->text = $text;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$telegramService = new TelegramService();
$telegramService->sendMessage($this->telegramId, $this->text, 'markdown');
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Jobs;
use App\Models\StatServer;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StatServerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $statistic;
public $tries = 3;
public $timeout = 5;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(array $statistic)
{
$this->onQueue('stat_server');
$this->statistic = $statistic;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$statistic = $this->statistic;
$data = StatServer::where('record_at', $statistic['record_at'])
->where('server_id', $statistic['server_id'])
->first();
if ($data) {
try {
$data->update($statistic);
} catch (\Exception $e) {
abort(500, '节点统计数据更新失败');
}
} else {
if (!StatServer::create($statistic)) {
abort(500, '节点统计数据创建失败');
}
}
}
}

12
app/Models/Knowledge.php Normal file
View File

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

View File

@ -4,9 +4,9 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tutorial extends Model
class Payment extends Model
{
protected $table = 'v2_tutorial';
protected $table = 'v2_payment';
protected $dateFormat = 'U';
protected $guarded = ['id'];
}

View File

@ -3,11 +3,10 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class ServerLog extends Model
{
protected $table = 'v2_server_log';
protected $dateFormat = 'U';
protected $dispatchesEvents = [
];
}

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