930 Commits
1.2.3 ... 1.5.4

Author SHA1 Message Date
339ab3925f Merge pull request #492 from v2board/dev
1.5.4
2022-01-01 00:02:25 +08:00
927d575255 update: fix admin dark mode 2022-01-01 00:01:06 +08:00
796bcc4ab3 Merge pull request #491 from v2board/dev
update: fix admin dark mode
2021-12-31 23:56:12 +08:00
552a80f5f9 update: fix admin dark mode 2021-12-31 23:55:23 +08:00
d78a8a2a9f Merge pull request #490 from v2board/dev
1.5.4
2021-12-31 23:30:59 +08:00
3fad649377 update: cache version 2021-12-31 23:30:34 +08:00
389db6fb97 Merge pull request #489 from v2board/dev
1.5.4
2021-12-31 23:10:22 +08:00
080af12f39 Merge pull request #487 from Mr-Sheep/master
fix issue with surge subscribe
2021-12-31 23:09:57 +08:00
ecbdd12a6d Merge pull request #488 from betaxab/updaterules
rules: update clash & surge rules
2021-12-31 23:08:34 +08:00
3ade2bf89c update: code optimization 2021-12-31 01:49:27 +08:00
45a5022d82 update: logic optimization 2021-12-31 01:48:21 +08:00
e1a8d6ca35 update: logic optimization 2021-12-30 00:20:30 +08:00
5df2504f73 update: fix 2021-12-28 15:12:25 +08:00
708a83017b rules: update clash & surge rules
1. linkedin will be proxy
2. no longer use jsdelivr, avoid ruleset resources failing to load
3. update app.clash.yaml
2021-12-28 13:29:04 +08:00
edb2e7956c update: sql 2021-12-28 12:04:30 +08:00
a557aa6d32 update: add coupon limit period 2021-12-28 01:59:59 +08:00
6ed9cc559e update: more 2021-12-28 01:49:33 +08:00
6718a61890 update: support stash 2021-12-27 00:13:27 +08:00
333611da48 update: support stash 2021-12-27 00:05:28 +08:00
8ee7839d00 update: admin comission manage 2021-12-26 20:13:53 +08:00
e29261a87f update: get reset day 2021-12-26 19:35:24 +08:00
ba75d6e993 update: add send mass mail queue 2021-12-26 19:21:29 +08:00
ce8b5dde28 update: fix telegram markdown 2021-12-23 14:25:05 +08:00
55357a1b0e update: fix markdown parse 2021-12-17 13:35:54 +08:00
0f777f1a77 update: add telegram discuss link & ui 2021-12-17 01:53:03 +08:00
8fa73c2d4b update: remove favicon 2021-12-15 22:13:06 +08:00
80ae5f17b7 update: invite copy link 2021-12-15 21:35:01 +08:00
de4f01f653 update: invite copy link 2021-12-15 21:23:06 +08:00
bbfabdb72f update: add clash regex filter 2021-12-13 14:26:05 +08:00
b392fa3345 update: payment icon 2021-12-13 14:24:53 +08:00
ecfb9ce8b0 Fix Surge Subscribing issue 2021-12-11 21:24:27 +08:00
a69eb4058b update: ui & payment icons 2021-12-07 16:34:44 +08:00
8d56377c8a update: fix user commission show 2021-12-02 14:03:39 +08:00
30aec3d8e9 update: add test send mail 2021-11-30 16:23:46 +08:00
05769ea591 update: fix some bug 2021-11-24 13:33:42 +08:00
fb8bcdcbe0 update: fix user filter 2021-11-24 13:15:28 +08:00
39c47a06eb update: fix darkreader 2021-11-23 23:05:55 +08:00
4c71eb5633 update: fix dark mode qrcode 2021-11-23 01:42:00 +08:00
b8df411ffe update: fix darkrader 2021-11-22 03:20:52 +08:00
66728f13ea update: ui components language 2021-11-15 02:35:13 +08:00
7b1a8ee5ff update: add custom currency 2021-11-14 02:43:41 +08:00
9d96d68f12 update: reset traffic 2021-11-11 01:25:45 +08:00
d0947d1aaa update: fix coupon use 2021-11-01 02:31:27 +08:00
6f67096fe3 update: order callback no filter & save notice 2021-10-31 21:08:14 +08:00
44b369d0e7 update: fix subscribe loading 2021-10-28 14:59:48 +08:00
04fcd6758d update: fix v2ray config edit 2021-10-27 12:59:25 +08:00
99f3004d6b update: remove get subscribe id 2021-10-25 01:44:51 +08:00
56c726b173 update: fix payment update 2021-10-22 00:00:49 +08:00
37a6f3861c update: add payment save request validate 2021-10-18 17:10:40 +08:00
90211c1018 update: fix user filter 2021-10-16 20:47:27 +08:00
cc3e3b3fd1 update: fix background 2021-10-15 15:14:25 +08:00
10d3feb57c update: check order 2021-10-14 15:45:42 +08:00
eb98d706ae update: shell 2021-10-13 21:23:18 +08:00
4996c317c9 update: install 2021-10-13 21:04:23 +08:00
f380bd3c0e update: install 2021-10-13 21:04:02 +08:00
8e7f0bbc44 update: shell 2021-10-13 21:00:21 +08:00
53011cb95c Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-10-11 21:46:45 +08:00
037e22aa2d update: getInfo show uuid 2021-10-11 21:39:55 +08:00
ffd5eb269d Merge pull request #485 from betaxab/update-surfboard 2021-10-11 00:28:09 +09:00
68def9b4de update: fix filter 2021-10-10 01:07:58 +08:00
a4d617608f update: fix filter 2021-10-09 23:21:39 +08:00
364e259188 update: fix v2ray config drawer 2021-10-08 20:29:55 +08:00
b18c810f7f update: fix v2ray config drawer 2021-10-08 20:00:34 +08:00
239eb2075c Protocols: Surfboard: The SS can now be directly supported 2021-10-07 23:01:39 +08:00
b40272a8fa update: fix user generate 2021-10-06 00:20:28 +08:00
a6d5a433b0 update: composer 2021-10-05 21:03:27 +08:00
fe0f0afa53 update: dark mode 2021-10-04 23:10:11 +08:00
9ff524fd15 update: fix context menu 2021-10-04 21:45:17 +08:00
f6d9e6e7f6 update: fix v2ray network settings check 2021-10-04 14:37:32 +08:00
d911069e18 update: fix table context menu 2021-10-04 12:07:55 +08:00
32c539d2c0 Merge pull request #484 from v2board/dev
1.5.3
2021-10-03 22:19:10 +09:00
52fa1ce6af update: language 2021-10-01 20:18:40 +09:00
82d2d91582 update: user 2021-10-01 19:30:49 +09:00
8085c2ba6a update: schedule 2021-10-01 17:37:49 +09:00
b60fde5762 update: fix charts 2021-09-27 14:58:58 +09:00
3b51f12ab1 update: rollback app rule 2021-09-26 22:09:19 +09:00
ab4e66a5b6 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-09-26 22:07:34 +09:00
1856e0e87e update: fix support safari 15 theme color 2021-09-26 22:07:24 +09:00
e20ae29fb1 Merge pull request #483 from betaxab/fix-clash-rule-for-gp
rules: clash: Solve Google Play app download issue on China phones
2021-09-26 20:57:06 +09:00
cfd528739a update: fix en-us 2021-09-26 20:44:47 +09:00
dd23609658 update: change charts component 2021-09-25 00:33:11 +09:00
c7f3cf9e67 update: support safari theme 2021-09-24 01:50:58 +09:00
ddf2f45d6c update: user filter 2021-09-22 21:13:20 +09:00
ba3de90733 update: fix commission 2021-09-22 18:04:16 +09:00
43f4ecce93 update: fix 2021-09-21 22:30:00 +09:00
30b3587771 update: fix 2021-09-21 19:11:14 +09:00
6ab9a4d54d update: email notice default off 2021-09-21 19:07:53 +09:00
7a4bd468a2 update: add plan reset method 2021-09-21 18:51:53 +09:00
c97d37a070 rules: Solve Google Play app download issue on China phones
Update: app.clash.yaml up-to-date
2021-09-20 10:52:37 +08:00
25b3b11efd update: add commission distribution 2021-09-18 21:04:35 +09:00
edfc4043e8 update: add commission distribution 2021-09-18 20:53:34 +09:00
ab9abf5b93 update: crisp more data 2021-09-14 20:31:56 +09:00
6dd199631b update: crisp more data 2021-09-14 20:07:57 +09:00
4b863d681e update: crisp more data 2021-09-14 19:47:57 +09:00
5d6010045d update: support md5 with sha256 2021-09-14 13:12:44 +09:00
0374a03892 update: support md5 with sha256 2021-09-14 13:10:29 +09:00
ec00fc4496 update: support md5 with sha256 2021-09-14 02:38:24 +09:00
a365357770 update: fix stat 2021-09-07 12:49:35 +09:00
14579d1eea update: update alert 2021-09-07 03:49:04 +09:00
9f6d1ada93 update: v2board install 2021-09-07 03:18:45 +09:00
895976b830 update: horizon config 2021-09-07 01:47:29 +09:00
4b011225db Merge pull request #482 from betaxab/subs-domain-force-direct 2021-09-07 01:33:37 +09:00
243aed3f55 Merge pull request #480 from betaxab/horizon-metrics 2021-09-07 01:33:24 +09:00
d25d7ba69b Merge pull request #479 from betaxab/update 2021-09-07 01:33:16 +09:00
c0c84efb7c Protocols: force the current subscription domain to be a direct rule 2021-09-06 18:26:03 +08:00
91c7dfc0ed update: add no reset method 2021-09-06 02:31:45 +09:00
6830e6af38 update: push tag 2021-09-06 01:24:41 +09:00
cb75579772 update: fix push tag 2021-09-06 01:19:43 +09:00
e980c2d8f3 update: set horizon with mem 2021-09-05 18:05:25 +09:00
b6195494d3 Kernel: add horizon snapshot schedule 2021-09-05 10:32:33 +08:00
ccf3497241 update: register api 2021-09-04 21:41:57 +09:00
c80d93fa25 update: order queue 2021-09-04 16:31:25 +09:00
c2577e37c4 update: server log 2021-09-03 00:31:12 +09:00
c183462ef3 update: server log 2021-09-02 21:09:04 +09:00
2a5e9ef079 update: rollback 2021-09-02 20:28:24 +09:00
decbae1413 update: traffic fetch job 2021-09-02 19:58:04 +09:00
0c14652ff7 update: server log fetch 2021-09-02 18:42:36 +09:00
91418caf04 update: log record 2021-09-01 03:32:15 +09:00
607aa82f88 update: horizon config 2021-09-01 03:08:31 +09:00
adc2d02c49 update: horizon config 2021-09-01 03:08:07 +09:00
fabb49baea update: horizon auth 2021-09-01 02:11:19 +09:00
35e11a6816 update: add horizon 2021-09-01 01:56:16 +09:00
1d87a1b99a update: traffic fetch queue 2021-09-01 00:55:07 +09:00
fbced4d09b update: fix order assign modal 2021-08-31 20:59:56 +09:00
52914e354e update: order assign notice 2021-08-31 20:54:08 +09:00
d392d29b50 update: reset text 2021-08-30 19:11:16 +09:00
347a3bb4b0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-30 01:18:17 +09:00
7ac1f69a71 update: fix css 2021-08-30 01:18:06 +09:00
6317c4a4f3 Protocols: Clash: update 2021-08-29 21:44:51 +08:00
3da4de02d0 Merge pull request #478 from betaxab/fix-clash-appname
Protocols: Clash: fix Clash app_name not fould
2021-08-29 22:38:38 +09:00
85686df2e6 Protocols: Clash: fix Clash app_name not found 2021-08-29 21:32:46 +08:00
adb5d041e6 fix: remind expire 2021-08-29 12:56:55 +09:00
7ba0e9a4e0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:55:08 +09:00
99a64f41e1 Merge pull request #477 from betaxab/abandon-clash-provider
Protocols: Clash: Rollback the previous config
2021-08-29 12:54:58 +09:00
62053bc30f Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:54:32 +09:00
e5e7a06514 update: fix coupon 2021-08-29 12:54:15 +09:00
905e2dacd1 Protocols: Clash: Rollback the previous configuration
because the provider cannot test the delay manually
2021-08-29 11:20:55 +08:00
8189a442bc Merge pull request #475 from WayChan/update_clash_subscribe
update Clash Subscribe
2021-08-29 02:52:51 +09:00
b38f7979d6 update: coupon 2021-08-28 16:34:22 +09:00
5a3b897c57 update: add coupon per user limit 2021-08-28 16:32:55 +09:00
2d5fb03937 update: remind mail i18n 2021-08-28 15:39:10 +09:00
982c47d0b4 update: remind mail i18n 2021-08-28 15:32:38 +09:00
4cebeed2d7 update: server log 2021-08-28 15:22:51 +09:00
c95d374cc0 update: order manual 2021-08-28 15:11:59 +09:00
23462e2753 update: add more tips 2021-08-27 00:54:35 +09:00
b26a3e0e70 update: fix order update 2021-08-25 18:06:08 +09:00
3600c9a166 update: rollback traffic fetch 2021-08-24 16:53:53 +09:00
8a58a1ad88 update 2021-08-24 16:50:45 +09:00
968d55e2e3 update: readme 2021-08-22 15:46:49 +09:00
98ac5cb680 Fix Clash Json decode error 2021-08-22 00:09:37 +08:00
0288d2df4b update: order success handle 2021-08-21 20:52:00 +09:00
cb8e34fa2e change profile-update-interval time 2021-08-21 17:59:58 +08:00
b0c818c661 update 2021-08-21 16:20:16 +09:00
303d4a1c66 update: short payment generate key 2021-08-20 23:42:07 +09:00
e6416712f0 Merge pull request #471 from betaxab/add-trojan-surfboard
Protocols: Surfboard: add trojan protocol support
2021-08-20 22:32:08 +09:00
5c6236366c Merge pull request #472 from betaxab/change-anxray-flag
Protocols: AnXray: change AnXray flag to axxray
2021-08-20 22:31:49 +09:00
9b0a487c69 update: add knowledge jump func 2021-08-20 22:30:18 +09:00
27a6cc98d1 update Clash Subscribe 2021-08-20 00:54:00 +08:00
e05f6116b6 Protocols: AnXray: change AnXray flag to axxray 2021-08-19 15:47:18 +08:00
a9db11877f Protocols: Surfboard: add trojan protocol support 2021-08-19 08:10:36 +08:00
abe1ebccae update: new generate order number method 2021-08-19 00:58:51 +09:00
c957a4ca83 update: fix ticket message length support utf8mb4 2021-08-18 21:23:45 +09:00
c9cd307cc4 update: auto open knowledge by id 2021-08-18 18:54:40 +09:00
31cdcef3aa update: remove apple id config 2021-08-12 17:28:15 +09:00
9f75d4cbde update: default theme add green color 2021-08-12 17:17:36 +09:00
de5f80b5a3 update: script 2021-08-10 13:37:37 +09:00
474df5e18f update: payment config format 2021-08-08 16:56:50 +09:00
dfa75f49bd update: fix tg 2021-08-07 19:00:33 +09:00
f2c7d092ac update: fix model 2021-08-06 23:41:41 +09:00
2c389ebe8c update: v2board update 2021-08-06 23:20:10 +09:00
25e19446e6 update: user tags 2021-08-06 17:33:16 +09:00
a8761e9d4d update: opt editor 2021-08-06 14:02:47 +09:00
5f4e9c0301 update: cache version 2021-08-06 13:51:55 +09:00
b6e9260464 update: fix editor 2021-08-06 13:51:30 +09:00
6aa96fe856 update: fix editor 2021-08-06 13:01:17 +09:00
ab02935fd7 update: fix v2board udpate 2021-08-06 02:40:31 +09:00
b275ad469b update: cache version 2021-08-06 01:53:51 +09:00
e6a7c2c11c update: opt code 2021-08-06 01:43:01 +09:00
0f0f726269 update: fix code support php8 2021-08-05 15:22:15 +09:00
00cd3e26be update: fix admin editor 2021-08-05 14:48:24 +09:00
d95974019a update: fix 2021-08-02 03:00:01 +09:00
7a80950ab5 update: laravel 7 2021-08-01 23:56:11 +09:00
73a6d3236a update: fix reset traffic maybe failure issue 2021-08-01 16:48:00 +09:00
59dd34674e update: rollback ui 2021-07-31 16:24:12 +09:00
5dda531c2b update: ui 2021-07-31 15:15:05 +09:00
7234ccf4c1 update: ui 2021-07-31 14:17:48 +09:00
448b5382b9 Merge pull request #468 from betaxab/fix-grpc-protocol 2021-07-31 13:17:06 +09:00
bd2b056fbf protocols: fix gRPC protocol serviceName field
fix anXray serverName filed
2021-07-31 12:14:40 +08:00
ad8e2b8e80 update: api 2021-07-31 03:36:49 +09:00
8a20a70513 update: fix ui 2021-07-31 02:34:58 +09:00
06adaa2124 update: fix order statistics 2021-07-31 02:21:49 +09:00
ebf98d42a8 update: new feature 2021-07-31 02:05:39 +09:00
1adb1bcfa0 update: fix default app_url with subscribe_url 2021-07-30 23:53:37 +09:00
bb8da6e2c5 update: fix payment verify app_url 2021-07-30 16:00:20 +09:00
c426aabbcf update: theme 2021-07-29 21:29:53 +09:00
18bb1bd962 update: theme 2021-07-29 21:24:37 +09:00
07e9377417 update: theme 2021-07-29 21:22:07 +09:00
c0d3150461 update: add payment app_url alert 2021-07-29 21:12:11 +09:00
1a9b8b09bb Merge pull request #467 from v2board/dev
1.5.2
2021-07-29 20:59:52 +09:00
cb621a93ae update: version 2021-07-29 20:56:28 +09:00
fb449b490e Merge pull request #466 from v2board/dev
1.5.2
2021-07-29 20:15:19 +09:00
e35ec76f81 update: add subscribe anxray 2021-07-29 20:13:01 +09:00
5a60380765 update: add theme config 2021-07-29 13:38:13 +09:00
f409d89c4a update: add config 2021-07-29 02:41:31 +09:00
de045c79f5 update: custom theme 2021-07-29 02:36:51 +09:00
6a336d4253 update: web route 2021-07-29 02:29:15 +09:00
0ce3948c12 update: theme directory 2021-07-29 02:21:09 +09:00
7338c4a294 update: user 2021-07-27 19:16:49 +09:00
9417623fcf fix: email suffix select 2021-07-27 18:32:29 +09:00
4f29264de9 update: fix email suffix 2021-07-26 13:50:04 +09:00
34d8f0d5f0 update: statistics check time 2021-07-24 01:31:23 +09:00
b585038916 update: user 2021-07-23 20:43:09 +09:00
5abb642277 update: user 2021-07-22 22:36:48 +09:00
01cf486137 update: comm config 2021-07-22 22:32:42 +09:00
cb811bda5a update: user 2021-07-22 21:12:09 +09:00
2306e38d0c update: remove aliyun repo 2021-07-22 01:22:14 +09:00
2798c1df06 update: user language 2021-07-20 21:16:46 +09:00
a727745b43 update: order ui 2021-07-20 18:22:26 +09:00
af901291a5 update: quick login url 2021-07-19 21:17:34 +09:00
42d755c7d9 update: get comm config api 2021-07-19 18:44:40 +09:00
56a4ce5fe4 fix: surplus 2021-07-18 22:27:23 +09:00
67ab0d1f22 update: user 2021-07-16 18:06:11 +09:00
230470c037 fix: knowledge sort 2021-07-16 17:46:36 +09:00
77aec7d553 update: commission type and opt knowledge sort timestamp 2021-07-14 20:08:30 +09:00
88948eb8ee fix: change plan refund surplus amount 2021-07-11 01:13:09 +09:00
4d38ce3968 Merge pull request #463 from betaxab/fix-vmss-tls 2021-07-09 20:24:25 +09:00
a88121626b fix: knowledge subscribe url 2021-07-09 00:01:13 +09:00
90409b1107 update: add default subscribe, 1.5.3 remove this 2021-07-07 19:54:31 +09:00
04615688f7 update: subscribe url random 2021-07-06 18:04:59 +09:00
f48de9f07d Protocols: fix vmess tls sni field 2021-07-05 21:01:28 +08:00
4722379e79 update: add new client protocol 2021-07-04 13:21:10 +09:00
9aed2554ee Merge pull request #462 from betaxab/routerprotocol
Protocols: add Passwall & SSRPlus subscription support
2021-07-04 13:08:07 +09:00
7eb3d9fed7 Protocols: add Passwall & SSRPlus subscription support
Usage:
add &flag=passwall at the end of subscription link for OpenWRT Passwall Luci Plugin
add &flag=ssrplus at the end of subscription link for OpenWRT ShadowsocksR Plus+ Luci Plugin
2021-07-03 23:02:47 +08:00
567acdd03b Merge pull request #461 from betaxab/v2rayprotocol
Protocols: add V2ray Client Protocol
2021-07-03 23:56:00 +09:00
a0d18d93d3 Protocols: add V2ray Client Protocol
Usage:
add &flag=v2rayng at the end of subscription link for V2rayNG Client
add &flag=v2rayn at the end of subscription link for V2rayN Client
2021-07-03 22:54:13 +08:00
1c419283c0 Merge pull request #460 from betaxab/p3
Protocols: fix QuantumultX subscription
2021-07-03 14:26:42 +09:00
d25f6bff65 Protocols: fix QuantumultX subscription 2021-07-03 12:53:22 +08:00
0b97dd0995 fix: app subscribe 2021-07-03 03:31:28 +09:00
6f90c6b878 update: code 2021-07-02 23:35:10 +09:00
5b8591fde9 update: code 2021-07-02 23:34:42 +09:00
dfeec044ea fix: statsistics 2021-07-02 22:43:22 +09:00
70b47ec4b0 update: remove origin subscribe method 2021-07-02 22:24:39 +09:00
cd85fba9c7 restore: origin subscribe method 2021-07-02 22:14:07 +09:00
e01e951f7f update: code 2021-07-02 22:07:54 +09:00
0284e47155 fix: shadowrocket grpc 2021-07-02 22:05:12 +09:00
a4e1ba4016 fix: shadowrocket grpc 2021-07-02 21:57:00 +09:00
f95deb3f16 fix: shadowrocket grpc 2021-07-02 21:52:13 +09:00
dfef6d2d94 update: remove origin subscribe method 2021-07-02 21:25:26 +09:00
efc8419eb5 update: grpc 2021-07-02 20:46:16 +09:00
aecfa85efd update 2021-07-01 23:25:59 +09:00
9db5de09f2 update: order process event 2021-07-01 22:04:22 +09:00
b174403a2a remove: soft delete 2021-07-01 20:06:09 +09:00
0f488540f4 update: middleware 2021-07-01 18:16:27 +09:00
d00ad94c46 update: sql 2021-07-01 18:14:49 +09:00
5fa7c534ff update: middleware 2021-07-01 18:11:23 +09:00
386c1339f5 fix: plan update 2021-07-01 18:02:13 +09:00
6509091e4f update: check order php env memory limit 2021-07-01 12:41:34 +09:00
078dfbf339 fix: safe delete sql 2021-07-01 03:01:26 +09:00
de6ff1dca9 update: add user softdelete api 2021-07-01 00:35:22 +09:00
044d1e9b7f update: message box 2021-06-30 00:48:28 +09:00
9c711b4ea6 update: middleware 2021-06-25 22:35:14 +09:00
760d248e4e update: ui 2021-06-23 19:36:40 +09:00
1a2e8e2966 fix: ui event 2021-06-23 19:24:30 +09:00
b00b58d73d update: ui 2021-06-23 19:15:19 +09:00
43027660be update: ui 2021-06-23 19:11:56 +09:00
fe69a5976b update: ui 2021-06-23 19:06:48 +09:00
ecebba1d51 fix: language change 2021-06-23 18:02:35 +09:00
efc43dc45e update: ui 2021-06-23 16:32:25 +09:00
c5d88bfbfc update: ui 2021-06-23 16:25:55 +09:00
6928fd3fef update readme 2021-06-23 02:31:50 +09:00
e4f178e167 add: wechatpay native 2021-06-14 22:29:31 +09:00
343c93ad8e add: wechatpay native 2021-06-14 22:27:29 +09:00
f80af8efdc update: dev version 2021-06-12 16:49:34 +09:00
e5b998ee8d fix: user update 2021-06-12 16:46:58 +09:00
d510064732 update: support grpc for clash & shadowrocket 2021-06-12 15:42:44 +09:00
c5e2ec1d12 update: gitignore 2021-06-12 01:57:49 +09:00
ee2ca23487 update: language more 2021-06-12 01:56:39 +09:00
a5532490ba update: telegram ticket replay notify 2021-06-10 22:24:54 +09:00
2f175323a3 update: css 2021-06-08 20:30:36 +09:00
f896370e64 update: sort leave alert 2021-06-08 20:07:10 +09:00
0313c35dbe update: user edit 2021-06-04 00:56:28 +09:00
232cb18a25 update: frontend 2021-06-02 21:10:16 +09:00
04a05a6ba4 Merge pull request #445 from betaxab/p1
ClientController: clash: add subscription-userinfo header
2021-05-26 23:26:35 +09:00
e2b4df592d update: frontend 2021-05-26 23:15:18 +09:00
668663f978 update: user frontend 2021-05-26 22:11:03 +09:00
2e7544c71c ClientController: clash: add subscription-userinfo header 2021-05-26 17:07:38 +08:00
1fb3f62cff update: user frontend 2021-05-26 17:35:57 +09:00
02a1728bff update: payment service 2021-05-26 01:38:57 +09:00
4ea71d85be update: user frontend 2021-05-24 22:49:57 +09:00
afe8bb3171 update: check order 2021-05-24 21:10:54 +09:00
ef17be2046 fix: ticket filter 2021-05-22 14:18:27 +09:00
53dea06a6c update: user 2021-05-21 02:40:31 +09:00
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
261 changed files with 13060 additions and 3313 deletions

2
.gitignore vendored
View File

@ -9,6 +9,7 @@
.env.backup
.phpunit.result.cache
.idea
.lock
Homestead.json
Homestead.yaml
npm-debug.log
@ -17,3 +18,4 @@ composer.phar
composer.lock
yarn.lock
docker-compose.yml
.DS_Store

View File

@ -1,16 +0,0 @@
php:
preset: laravel
enabled:
- alpha_ordered_imports
disabled:
- length_ordered_imports
- unused_use
finder:
not-name:
- index.php
- server.php
js:
finder:
not-name:
- webpack.mix.js
css: true

View File

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

View File

@ -2,13 +2,13 @@
namespace App\Console\Commands;
use App\Jobs\OrderHandleJob;
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
{
@ -43,88 +43,12 @@ class CheckOrder extends Command
*/
public function handle()
{
$orders = Order::get();
foreach ($orders as $item) {
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$orderService = new OrderService($item);
$orderService->cancel();
}
break;
case 1:
$this->orderHandle($item);
break;
}
}
}
private function orderHandle(Order $order)
{
$user = User::find($order->user_id);
$plan = Plan::find($order->plan_id);
if ((string)$order->cycle === 'onetime_price') {
return $this->buyByOneTime($order, $user, $plan);
}
return $this->buyByCycle($order, $user, $plan);
}
private function buyByCycle(Order $order, User $user, Plan $plan)
{
// change plan process
if ((int)$order->type === 3) {
$user->expired_at = time();
}
if ($order->refund_amount) {
$user->balance = $user->balance + $order->refund_amount;
}
$user->transfer_enable = $plan->transfer_enable * 1073741824;
if ((int)config('v2board.renew_reset_traffic_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 buyByOneTime(Order $order, User $user, Plan $plan)
{
if ($order->refund_amount) {
$user->balance = $user->balance + $order->refund_amount;
}
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->u = 0;
$user->d = 0;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = NULL;
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);
ini_set('memory_limit', -1);
$orders = Order::whereIn('status', [0, 1])
->orderBy('created_at', 'ASC')
->get();
foreach ($orders as $order) {
OrderHandleJob::dispatch($order->trade_no);
}
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Services\ServerService;
use App\Services\TelegramService;
use App\Utils\CacheKey;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class CheckServer extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:server';
/**
* 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->checkOffline();
}
private function checkOffline()
{
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($servers as $server) {
if ($server['parent_id']) continue;
if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) {
$telegramService = new TelegramService();
$message = sprintf(
"节点掉线通知\r\n----\r\n节点名称:%s\r\n节点地址:%s\r\n",
$server['name'],
$server['host']
);
$telegramService->sendMessageWithAdmin($message);
Cache::forget(CacheKey::get(sprintf("SERVER_%s_LAST_CHECK_AT", strtoupper($server['type'])), $server->id));
}
}
}
}

View File

@ -2,11 +2,14 @@
namespace App\Console\Commands;
use App\Models\Plan;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetTraffic extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
@ -29,6 +32,8 @@ class ResetTraffic extends Command
public function __construct()
{
parent::__construct();
$this->builder = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
}
/**
@ -38,42 +43,69 @@ class ResetTraffic extends Command
*/
public function handle()
{
$user = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// 1 a month
case 0:
$this->resetByMonthFirstDay($user);
break;
// expire day
case 1:
$this->resetByExpireDay($user);
break;
ini_set('memory_limit', -1);
foreach (Plan::get() as $plan) {
switch ($plan->reset_traffic_method) {
case null: {
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($this->builder);
break;
// expire day
case 1:
$this->resetByExpireDay($this->builder);
break;
// no action
case 2:
break;
}
break;
}
case 0: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByMonthFirstDay($builder);
break;
}
case 1: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByExpireDay($builder);
break;
}
case 2: {
break;
}
}
}
}
private function resetByMonthFirstDay($user):void
private function resetByMonthFirstDay($builder):void
{
if ((string)date('d') === '01') {
$user->update([
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay($user):void
private function resetByExpireDay($builder):void
{
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($user->get() as $item) {
foreach ($builder->get() as $item) {
$expireDay = date('d', $item->expired_at);
if ($expireDay === date('d') || (string)$lastDay === '29' || (string)$lastDay === '30') {
$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([
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Services\MailService;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
@ -41,51 +42,9 @@ class SendRemindMail extends Command
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $this->remindExpire($user);
if ($user->remind_traffic) $this->remindTraffic($user);
if ($user->remind_expire) $mailService->remindExpire($user);
}
}
private function remindExpire($user)
{
if (($user->expired_at - 86400) < time() && $user->expired_at > time()) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的服务即将到期',
'template_name' => 'remindExpire',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
}
}
private function remindTraffic($user)
{
if ($this->remindTrafficIsWarnValue(($user->u + $user->d), $user->transfer_enable)) {
$sendCount = MailLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('template_name', 'mail.sendRemindTraffic')
->count();
if ($sendCount > 0) return;
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的流量使用已达到80%',
'template_name' => 'remindTraffic',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
}
}
private function remindTrafficIsWarnValue($ud, $transfer_enable)
{
if ($ud <= 0) return false;
if (($ud / $transfer_enable * 100) < 80) return false;
return true;
}
}

View File

@ -2,29 +2,29 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\Order;
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Matriphe\Larinfo;
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.

View File

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

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

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

@ -25,15 +25,17 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// v2board
$schedule->command('v2board:cache')->hourly();
$schedule->command('v2board:statistics')->dailyAt('0:10');
// check
$schedule->command('check:order')->everyMinute();
$schedule->command('check:commission')->everyMinute();
// reset
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->monthly();
$schedule->command('reset:serverLog')->quarterly()->at('0:15');
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
/**

View File

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

View File

@ -3,9 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Mail;
class ConfigController extends Controller
{
@ -20,6 +23,49 @@ class ConfigController extends Controller
]);
}
public function getThemeTemplate()
{
$path = public_path('theme/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function testSendMail(Request $request)
{
$obj = new SendEmailJob([
'email' => $request->session()->get('email'),
'subject' => 'This is v2board test email',
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'content' => 'This is v2board test email',
'url' => config('v2board.app_url')
]
]);
return response([
'data' => true,
'log' => $obj->handle()
]);
}
public function setTelegramWebhook(Request $request)
{
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$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
@ -30,7 +76,15 @@ class ConfigController extends Controller
'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),
'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1)
'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),
'commission_distribution_enable' => config('v2board.commission_distribution_enable', 0),
'commission_distribution_l1' => config('v2board.commission_distribution_l1'),
'commission_distribution_l2' => config('v2board.commission_distribution_l2'),
'commission_distribution_l3' => config('v2board.commission_distribution_l3')
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
@ -43,12 +97,22 @@ class ConfigController extends Controller
'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'),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
],
'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
'reset_traffic_method' => (int)config('v2board.reset_traffic_method', 0),
'renew_reset_traffic_enable' => (int)config('v2board.renew_reset_traffic_enable', 1)
'surplus_enable' => (int)config('v2board.surplus_enable', 1),
'new_order_event_id' => (int)config('v2board.new_order_event_id', 0),
'renew_order_event_id' => (int)config('v2board.renew_order_event_id', 0),
'change_order_event_id' => (int)config('v2board.change_order_event_id', 0),
],
'pay' => [
// alipay
@ -59,33 +123,66 @@ class ConfigController extends Controller
// 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_webhook_key' => config('v2board.stripe_webhook_key'),
'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
// bitpayx
'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// paytaro
'paytaro_enable' => (int)config('v2board.paytaro_enable', 0),
'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', 'v2board'),
'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_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')
],
'tutorial' => [
'apple_id' => config('v2board.apple_id')
'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'),
],
'email' => [
'email_template' => config('v2board.email_template', 'default')
'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'),
'telegram_discuss_link' => config('v2board.telegram_discuss_link')
],
'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')
]
]
]);
@ -93,10 +190,10 @@ class ConfigController extends Controller
public function save(ConfigSave $request)
{
$data = $request->input();
$data = $request->validated();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改');
}
$array[$k] = $v;
@ -105,6 +202,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,33 +3,45 @@
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') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => Coupon::all()
'data' => $coupons,
'total' => $total
]);
}
public function save(CouponSave $request)
public function generate(CouponGenerate $request)
{
$params = $request->only([
'name',
'type',
'value',
'started_at',
'ended_at',
'limit_use'
]);
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$params = $request->validated();
if (!$request->input('id')) {
$params['code'] = Helper::randomChar(8);
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
abort(500, '创建失败');
}
@ -46,6 +58,38 @@ class CouponController extends Controller
]);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
$coupon['limit_period'] = json_encode($coupon['limit_period']);
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
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 = implode("/", json_decode($coupon['limit_plan_ids'], true)) ?? '不限制';
$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,113 @@
<?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();
try {
foreach ($request->input('knowledge_ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
}
} catch (\Exception $e) {
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,47 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\MailSend;
use App\Services\UserService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Jobs\SendEmailJob;
class MailController extends Controller
{
public function send(MailSend $request)
{
$userService = new UserService();
$users = [];
switch ($request->input('type')) {
case 1: $users = $userService->getAllUsers();
break;
case 2: $users = $userService->getUsersByIds($request->input('receiver'));
break;
// available users
case 3: $users = $userService->getAvailableUsers();
break;
// un available users
case 4: $users = $userService->getUnAvailbaleUsers();
break;
}
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
}
return response([
'data' => true
]);
}
}

View File

@ -2,34 +2,51 @@
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\Services\UserService;
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();
@ -47,10 +64,45 @@ class OrderController extends Controller
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'status',
'commission_status'
]);
@ -60,16 +112,6 @@ class OrderController extends Controller
abort(500, '订单不存在');
}
if (isset($params['status']) && (int)$params['status'] === 2) {
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
try {
$order->update($params);
} catch (\Exception $e) {
@ -81,23 +123,54 @@ class OrderController extends Controller
]);
}
public function repair(Request $request)
public function assign(OrderAssign $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数错误');
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该用户不存在');
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或订单已支付');
if (!$plan) {
abort(500, '该订阅不存在');
}
$order->status = 1;
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
abort(500, '该用户还有待支付的订单,无法分配');
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->period === '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()) {
abort(500, '保存失败');
DB::rollback();
abort(500, '订单创建失败');
}
DB::commit();
return response([
'data' => true
'data' => $order->trade_no
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\PaymentSave;
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 (!config('v2board.app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
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
]);
}
$request->validate([
'name' => 'required',
'payment' => 'required',
'config' => 'required'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空'
]);
if (!Payment::create([
'name' => $request->input('name'),
'icon' => $request->input('icon'),
'payment' => $request->input('payment'),
'config' => $request->input('config'),
'uuid' => Helper::randomChar(8)
])) {
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,6 +3,7 @@
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;
@ -15,23 +16,45 @@ 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->only(array_keys(PlanSave::RULES));
$params = $request->validated();
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
DB::beginTransaction();
// update user group id
// update user group id and transfer
try {
User::where('plan_id', $plan->id)->update(['group_id' => $plan->group_id]);
User::where('plan_id', $plan->id)->update([
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824
]);
$plan->update($params);
} catch (\Exception $e) {
DB::rollBack();
@ -91,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,85 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Models\Plan;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerV2ray;
use App\Models\ServerGroup;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class GroupController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('group_id')) {
return response([
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
$serverGroups = ServerGroup::get();
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0;
foreach ($servers as $server) {
if (in_array($v['id'], $server['group_id'])) {
$serverGroups[$k]['server_count'] = $serverGroups[$k]['server_count']+1;
}
}
}
return response([
'data' => $serverGroups
]);
}
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 = ServerV2ray::all();
foreach ($servers as $server) {
if (in_array($request->input('id'), $server->group_id)) {
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,54 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Models\ServerV2ray;
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();
return response([
'data' => $serverService->getAllServers()
]);
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
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 (!ServerV2ray::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,91 @@
<?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();
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,99 @@
<?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();
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,101 @@
<?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\ServerV2ray;
class V2rayController extends Controller
{
public function save(ServerV2raySave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerV2ray::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerV2rayUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerV2ray::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 = ServerV2ray::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerV2ray::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,193 +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['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 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, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
if (!Server::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@ -2,15 +2,21 @@
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;
use App\Models\Server;
use App\Models\ServerV2ray;
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' => ServerV2ray::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,76 +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::get()
]);
}
public function save(TutorialSave $request)
{
$params = $request->only(array_keys(TutorialSave::RULES));
if (!$request->input('id')) {
if (!Tutorial::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Tutorial::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$tutorial = Tutorial::find($request->input('id'));
if (!$tutorial) {
abort(500, '教程不存在');
}
$tutorial->show = $tutorial->show ? 0 : 1;
if (!$tutorial->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function 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,64 @@
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\Services\UserService;
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)
{
$filters = $request->input('filter');
if ($filters) {
foreach ($filters as $k => $filter) {
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['condition'], $filter['value'])->first();
$inviteUserId = isset($user->id) ? $user->id : 0;
$builder->where('invite_user_id', $inviteUserId);
unset($filters[$k]);
continue;
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
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 +70,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeHost() . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
}
return response([
'data' => $res,
@ -46,20 +83,18 @@ class UserController extends Controller
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::find($request->input('id'));
if ($user->invite_user_id) {
$user['invite_user'] = User::find($user->invite_user_id);
}
return response([
'data' => User::select([
'email',
'u',
'd',
'transfer_enable',
'expired_at'
])->find($request->input('id'))
'data' => $user
]);
}
public function update(UserUpdate $request)
{
$params = $request->only(array_keys(UserUpdate::RULES));
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
@ -69,6 +104,7 @@ class UserController extends Controller
}
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($params['password']);
}
@ -79,6 +115,14 @@ class UserController extends Controller
}
$params['group_id'] = $plan->group_id;
}
if ($request->input('invite_user_email')) {
$inviteUser = User::where('email', $request->input('invite_user_email'))->first();
if ($inviteUser) {
$params['invite_user_id'] = $inviteUser->id;
}
} else {
$params['invite_user_id'] = null;
}
try {
$user->update($params);
@ -89,4 +133,154 @@ class UserController extends Controller
'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()
];
if (User::where('email', $user['email'])->first()) {
abort(500, '邮箱已存在于系统中');
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
abort(500, '生成失败');
}
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')
]
],
'send_email_mass');
}
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

@ -3,12 +3,13 @@
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 App\Models\ServerV2ray;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
@ -16,96 +17,78 @@ 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);
}
$defaultConfig = base_path() . '/resources/rules/app.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.app.clash.yaml';
if (File::exists($customConfig)) {
$config = Yaml::parseFile($customConfig);
} else {
$config = Yaml::parseFile($defaultConfig);
}
$proxy = [];
$proxies = [];
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, Protocols\Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($user->expired_at > time()) {
$servers = Server::where('show', 1)
->orderBy('name')
->get();
foreach ($servers as $item) {
$groupId = json_decode($item['group_id']);
if (in_array($user->group_id, $groupId)) {
array_push($nodes, $item);
}
}
if ($item['type'] === 'v2ray') {
array_push($proxy, Protocols\Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, Protocols\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')
]
]);
}
public function config(Request $request)
{
if (empty($request->input('server_id'))) {
abort(500, '参数错误');
}
$user = $request->user;
if ($user->expired_at < time() && $user->expired_at !== NULL) {
abort(500, '订阅计划已过期');
}
$server = Server::where('show', 1)
->where('id', $request->input('server_id'))
->first();
if (!$server) {
abort(500, '服务器不存在');
}
$json = json_decode(self::CLIENT_CONFIG);
//socks
$json->inbound->port = (int)self::SOCKS_PORT;
//http
$json->inboundDetour[0]->port = (int)self::HTTP_PORT;
//other
$json->outbound->settings->vnext[0]->address = (string)$server->host;
$json->outbound->settings->vnext[0]->port = (int)$server->port;
$json->outbound->settings->vnext[0]->users[0]->id = (string)$user->v2ray_uuid;
$json->outbound->settings->vnext[0]->users[0]->alterId = (int)$user->v2ray_alter_id;
$json->outbound->settings->vnext[0]->remark = (string)$server->name;
$json->outbound->streamSettings->network = $server->network;
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->outbound->streamSettings->tcpSettings = json_decode($server->networkSettings);
break;
case 'kcp':
$json->outbound->streamSettings->kcpSettings = json_decode($server->networkSettings);
break;
case 'ws':
$json->outbound->streamSettings->wsSettings = json_decode($server->networkSettings);
break;
case 'http':
$json->outbound->streamSettings->httpSettings = json_decode($server->networkSettings);
break;
case 'domainsocket':
$json->outbound->streamSettings->dsSettings = json_decode($server->networkSettings);
break;
case 'quic':
$json->outbound->streamSettings->quicSettings = json_decode($server->networkSettings);
break;
}
}
if ($request->input('is_global')) {
$json->routing->settings->rules[0]->outboundTag = 'proxy';
}
if ($server->tls) {
$json->outbound->streamSettings->security = "tls";
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

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

@ -2,169 +2,40 @@
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Client\Protocols\V2rayN;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
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.
$userService = new UserService();
if ($userService->isAvailable($user)) {
$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);
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
if ($flag) {
foreach (glob(app_path('Http//Controllers//Client//Protocols') . '/*.php') as $file) {
$file = 'App\\Http\\Controllers\\Client\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
if (strpos($flag, $class->flag) !== false) {
die($class->handle());
}
}
}
// todo 1.5.3 remove
$class = new V2rayN($user, $servers);
die($class->handle());
die('该客户端暂不支持进行订阅');
}
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=' . ($item->tls ? 'wss' : 'ws');
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
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) {
$str = '';
$str .= $item->name . '= vmess, ' . $item->host . ', ' . $item->port . ', chacha20-ietf-poly1305, "' . $user->v2ray_uuid . '", over-tls=' . ($item->tls ? "true" : "false") . ', certificate=0, group=' . config('v2board.app_name', 'V2Board');
if ($item->network === 'ws') {
$str .= ', obfs=ws';
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $str .= ', obfs-path="' . $wsSettings->path . '"';
if (isset($wsSettings->headers->Host)) $str .= ', obfs-header="Host:' . $wsSettings->headers->Host . '"';
}
}
$uri .= "vmess://" . base64_encode($str) . "\r\n";
}
return base64_encode($uri);
}
private function origin($user, $server)
{
$uri = '';
foreach ($server as $item) {
$uri .= Helper::buildVmessLink($item, $user);
}
return base64_encode($uri);
}
private function clash($user, $server)
{
$proxy = [];
$proxyGroup = [];
$proxies = [];
$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;
$array['skip-cert-verify'] = true;
}
if ($item->network == 'ws') {
$array['network'] = $item->network;
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $array['ws-path'] = $wsSettings->path;
if (isset($wsSettings->headers->Host)) $array['ws-headers'] = [
'Host' => $wsSettings->headers->Host
];
}
}
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' => array_merge($proxies, [
'auto',
'fallback-auto'
])
]);
try {
$rules = [];
foreach (glob(base_path() . '/resources/rules/' . '*.clash.yaml') as $file) {
$rules = array_merge($rules, Yaml::parseFile($file)['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);
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class AnXray
{
public $flag = 'axxray';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($uuid, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$uuid}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildShadowsocksSIP008($uuid, $server)
{
$config = [
"id" => $server['id'],
"remarks" => $server['name'],
"server" => $server['host'],
"server_port" => $server['port'],
"password" => $uuid,
"method" => $server['cipher']
];
return $config;
}
public static function buildVmess($uuid, $server)
{
$config = [
"encryption" => "none",
"type" => urlencode($server['network']),
"security" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = urlencode($tlsSettings['serverName']);
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = urlencode($wsSettings['path']);
if (isset($wsSettings['headers']['Host'])) $config['host'] = urlencode($wsSettings['headers']['Host']);
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['serviceName'] = urlencode($grpcSettings['serviceName']);
}
return "vmess://" . $uuid . "@" . $server['host'] . ":" . $server['port'] . "?" . http_build_query($config) . "#" . urlencode($server['name']) . "\r\n";
}
public static function buildTrojan($uuid, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$uuid}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

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

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Passwall
{
public $flag = 'passwall';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class QuantumultX
{
public $flag = 'quantumult%20x';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$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 .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$config = [
"shadowsocks={$server['host']}:{$server['port']}",
"method={$server['cipher']}",
"password={$password}",
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"vmess={$server['host']}:{$server['port']}",
'method=chacha20-poly1305',
"password={$uuid}",
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
if ($server['tls']) {
if ($server['network'] === 'tcp')
array_push($config, 'obfs=over-tls');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'tls-verification=' . ($tlsSettings['allowInsecure'] ? 'false' : 'true'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$host = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
if ($server['tls'])
array_push($config, 'obfs=wss');
else
array_push($config, 'obfs=ws');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "obfs-uri={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']) && !isset($host))
$host = $wsSettings['headers']['Host'];
}
}
if (isset($host)) {
array_push($config, "obfs-host={$host}");
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"trojan={$server['host']}:{$server['port']}",
"password={$password}",
'over-tls=true',
$server['server_name'] ? "tls-host={$server['server_name']}" : "",
// Tips: allowInsecure=false = tls-verification=true
$server['allow_insecure'] ? 'tls-verification=false' : 'tls-verification=true',
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class SSRPlus
{
public $flag = 'ssrplus';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Shadowrocket
{
public $flag = 'shadowrocket';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
//display remaining traffic and expire date
$upload = round($user['u'] / (1024*1024*1024), 2);
$download = round($user['d'] / (1024*1024*1024), 2);
$totalTraffic = round($user['transfer_enable'] / (1024*1024*1024), 2);
$expiredDate = date('Y-m-d', $user['expired_at']);
$uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n";
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$userinfo = base64_encode('auto:' . $uuid . '@' . $server['host'] . ':' . $server['port']);
$config = [
'tfo' => 1,
'remark' => $server['name'],
'alterId' => $server['alter_id']
];
if ($server['tls']) {
$config['tls'] = 1;
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$config['allowInsecure'] = (int)$tlsSettings['allowInsecure'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['peer'] = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
$config['obfs'] = "websocket";
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$config['obfsParam'] = $wsSettings['headers']['Host'];
}
}
if ($server['network'] === 'grpc') {
$config['obfs'] = "grpc";
if ($server['networkSettings']) {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName']) && !empty($grpcSettings['serviceName']))
$config['path'] = $grpcSettings['serviceName'];
}
if (isset($tlsSettings)) {
$config['host'] = $tlsSettings['serverName'];
} else {
$config['host'] = $server['host'];
}
}
$query = http_build_query($config, '', '&', PHP_QUERY_RFC3986);
$uri = "vmess://{$userinfo}?{$query}";
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}&tfo=1#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Shadowsocks
{
public $flag = 'shadowsocks';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$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, self::SIP008($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);
}
public static function SIP008($server, $user)
{
$config = [
"id" => $server['id'],
"remarks" => $server['name'],
"server" => $server['host'],
"server_port" => $server['port'],
"password" => $user['uuid'],
"method" => $server['cipher']
];
return $config;
}
}

View File

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

View File

@ -0,0 +1,140 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Surfboard
{
public $flag = 'surfboard';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= self::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= self::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [Proxy]
$proxies .= self::buildTrojan($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
}
$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'];
$subsDomain = $_SERVER['SERVER_NAME'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
public static function buildShadowsocks($password, $server)
{
$config = [
"{$server['name']}=ss",
"{$server['host']}",
"{$server['port']}",
"encrypt-method={$server['cipher']}",
"password={$password}",
'tfo=true',
'udp-relay=true'
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"{$server['name']}=vmess",
"{$server['host']}",
"{$server['port']}",
"username={$uuid}",
'tfo=true',
'udp-relay=true'
];
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
array_push($config, "sni={$tlsSettings['serverName']}");
}
}
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
}
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"{$server['name']}=trojan",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
$server['server_name'] ? "sni={$server['server_name']}" : "",
'tfo=true',
'udp-relay=true'
];
if (!empty($server['allow_insecure'])) {
array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
}
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Surge
{
public $flag = 'surge';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= self::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= self::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [Proxy]
$proxies .= self::buildTrojan($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
}
$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
$subsDomain = $_SERVER['SERVER_NAME'];
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
public static function buildShadowsocks($password, $server)
{
$config = [
"{$server['name']}=ss",
"{$server['host']}",
"{$server['port']}",
"encrypt-method={$server['cipher']}",
"password={$password}",
'tfo=true',
'udp-relay=true'
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"{$server['name']}=vmess",
"{$server['host']}",
"{$server['port']}",
"username={$uuid}",
'tfo=true',
'udp-relay=true'
];
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
array_push($config, "sni={$tlsSettings['serverName']}");
}
}
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
}
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"{$server['name']}=trojan",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
$server['server_name'] ? "sni={$server['server_name']}" : "",
'tfo=true',
'udp-relay=true'
];
if (!empty($server['allow_insecure'])) {
array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
}
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class V2rayN
{
public $flag = 'v2rayn';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class V2rayNG
{
public $flag = 'v2rayng';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,36 @@
<?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'),
'is_email_verify' => (int)config('v2board.email_verify', 0) ? 1 : 0,
'is_invite_force' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'email_whitelist_suffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0,
'is_recaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'app_description' => config('v2board.app_description'),
'app_url' => config('v2board.app_url')
]
]);
}
private function getEmailSuffix()
{
$suffix = config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}

View File

@ -1,153 +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'],
'description' => config('v2board.app_name', 'V2Board') . $source['metadata']['invoice_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(json_encode([
'status' => 200
]));
}
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(isset($paymentService->customResult) ? $paymentService->customResult : '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->paid($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,200 @@
<?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');
$telegramService->sendMessageWithAdmin("#`{$ticketId}` 的工单已由 {$user->email} 进行回复", true);
}
}

View File

@ -14,45 +14,59 @@ 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, __('Invalid code is incorrect'));
}
}
if ((int)config('v2board.email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
abort(500, '邮箱后缀不处于白名单中');
abort(500, __('Email suffix is not in the Whitelist'));
}
}
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 alias is not supported'));
}
}
if ((int)config('v2board.stop_register', 0)) {
abort(500, '本站已关闭注册');
abort(500, __('Registration has closed'));
}
if ((int)config('v2board.invite_force', 0)) {
if (empty($request->input('invite_code'))) {
abort(500, '必须使用邀请码才可以注册');
abort(500, __('You must use the invitation code to register'));
}
}
if ((int)config('v2board.email_verify', 0)) {
if (empty($request->input('email_code'))) {
abort(500, '邮箱验证码不能为空');
abort(500, __('Email verification code cannot be empty'));
}
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
abort(500, __('Incorrect email verification code'));
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
abort(500, '邮箱已存在系统中');
abort(500, __('Email already exists'));
}
$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'))
@ -60,7 +74,7 @@ class AuthController extends Controller
->first();
if (!$inviteCode) {
if ((int)config('v2board.invite_force', 0)) {
abort(500, '邀请码无效');
abort(500, __('Invalid invitation code'));
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
@ -83,15 +97,20 @@ class AuthController extends Controller
}
if (!$user->save()) {
abort(500, '注册失败');
abort(500, __('Register failed'));
}
if ((int)config('v2board.email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$data = [
'token' => $user->token,
'auth_data' => base64_encode("{$user->email}:{$user->password}")
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
return response()->json([
'data' => true
'data' => $data
]);
}
@ -102,22 +121,24 @@ class AuthController extends Controller
$user = User::where('email', $email)->first();
if (!$user) {
abort(500, '用户名或密码错误');
abort(500, __('Incorrect email or password'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
abort(500, '用户名或密码错误');
abort(500, __('Incorrect email or password'));
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
abort(500, __('Your account has been suspended'));
}
$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 +146,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,34 +158,27 @@ 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, '令牌有误');
abort(500, __('Token error'));
}
$user = User::find($userId);
if (!$user) {
abort(500, '用户不存在');
abort(500, __('The user does not '));
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
abort(500, __('Your account has been suspended'));
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
@ -174,6 +192,46 @@ class AuthController extends Controller
}
}
public function getTempToken(Request $request)
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, __('Token error'));
}
$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')));
if (!isset($authData[0])) abort(403, __('Token error'));
$user = User::where('email', $authData[0])
->where('password', $authData[1])
->first();
if (!$user) {
abort(500, __('Token error'));
}
$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 = [
@ -190,16 +248,17 @@ class AuthController extends Controller
public function forget(AuthForget $request)
{
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
abort(500, __('Incorrect email verification code'));
}
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该邮箱不存在系统中');
abort(500, __('This email is not registered in the system'));
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, '重置失败');
abort(500, __('Reset failed'));
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return response([

View File

@ -13,9 +13,11 @@ use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\Dict;
use App\Utils\CacheKey;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
// TODO: remove on 1.5.5
public function config()
{
return response([
@ -24,7 +26,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')
]
]);
}
@ -38,12 +44,19 @@ class CommController extends Controller
public function sendEmailVerify(CommSendEmailVerify $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, __('Invalid code is incorrect'));
}
}
$email = $request->input('email');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
abort(500, '验证码已发送,请过一会再请求');
abort(500, __('Email verification code has been sent, please request again later'));
}
$code = rand(100000, 999999);
$subject = config('v2board.app_name', 'V2Board') . '邮箱验证码';
$subject = config('v2board.app_name', 'V2Board') . __('Email verification code');
SendEmailJob::dispatch([
'email' => $email,

View File

@ -3,18 +3,23 @@
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\ServerV2ray;
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":[]},"sniffing":{"enabled": true,"destOverride": ["http","tls"]},"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":{"rules":[{"inboundTag":"api","outboundTag":"api","type":"field"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
@ -30,24 +35,23 @@ class DeepbworkController extends Controller
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
$server = ServerV2ray::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put('server_last_check_at_' . $server->id, time());
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"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([
@ -59,32 +63,23 @@ class DeepbworkController extends Controller
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerV2ray::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();
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();
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess');
}
return response([
@ -103,7 +98,7 @@ class DeepbworkController extends Controller
}
$serverService = new ServerService();
try {
$json = $serverService->getConfig($nodeId, $localPort);
$json = $serverService->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}

View File

@ -1,129 +0,0 @@
<?php
namespace App\Http\Controllers\Server;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class PoseidonController extends Controller
{
CONST SERVER_CONFIG = '{"api":{"services":["HandlerService","StatsService"],"tag":"api"},"stats":{},"inbound":{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled": true,"destOverride": ["http","tls"]},"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":{"rules":[{"inboundTag":"api","outboundTag":"api","type":"field"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
// 后端获取用户
public function user(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
if (!$server) {
return $this->error("server could not be found", 404);
}
Cache::put('server_last_check_at_' . $server->id, time());
$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,
];
unset($user['v2ray_uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
}
return $this->success($result);
}
// 后端提交数据
public function submit(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
// 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);
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();
}
return $this->success('');
}
// 后端获取配置
public function config(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
return $this->error('invalid parameters', 400);
}
$serverService = new ServerService();
try {
$json = $serverService->getConfig($nodeId, $localPort);
$json->poseidon = [
'license_key' => (string)config('v2board.server_license'),
];
return $this->success($json);
} catch (\Exception $e) {
return $this->error($e->getMessage(), 500);
}
}
protected function verifyToken(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
return $this->error("token must be set");
}
if ($token !== config('v2board.server_token')) {
return $this->error("invalid token");
}
}
protected function error($msg, int $status = 400) {
return response([
'msg' => $msg,
], $status);
}
protected function success($data) {
return response([
'msg' => 'ok',
'data' => $data,
]);
}
}

View File

@ -0,0 +1,82 @@
<?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 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($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();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'shadowsocks');
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
}

View File

@ -0,0 +1,105 @@
<?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($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();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'trojan');
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@ -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,37 @@
<?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),
'telegram_discuss_link' => config('v2board.telegram_discuss_link'),
'stripe_pk' => config('v2board.stripe_pk_live'),
'withdraw_methods' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
]
]);
}
public function getStripePublicKey(Request $request)
{
$payment = Payment::where('id', $request->input('id'))
->where('payment', 'StripeCredit')
->first();
if (!$payment) abort(500, 'payment is not found');
return response([
'data' => $payment->config['stripe_pk_live']
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
use App\Models\Coupon;
@ -11,23 +12,14 @@ class CouponController extends Controller
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, '优惠券码不能为空');
}
$coupon = Coupon::where('code', $request->input('code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
}
if (time() < $coupon->started_at) {
abort(500, '优惠券还未到可用时间');
}
if (time() > $coupon->ended_at) {
abort(500, '优惠券已过期');
abort(500, __('Coupon cannot be empty'));
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->session()->get('id'));
$couponService->check();
return response([
'data' => $coupon
'data' => $couponService->getCoupon()
]);
}
}

View File

@ -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, __('The maximum number of creations has been reached'));
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->session()->get('id');
@ -28,7 +28,8 @@ class InviteController extends Controller
{
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('status', 3)
->where('commission_balance', '>', 0)
->whereIn('status', [3, 4])
->select([
'id',
'commission_status',

View File

@ -0,0 +1,72 @@
<?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, __('Article does 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 = __('No active subscription. Unable to use our provided Apple ID');
$appleIdPassword = __('No active subscription. Unable to use our provided Apple ID');
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = config('v2board.app_url', env('APP_URL'));
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
$subscribeUrl = "{$subscribeUrl}/api/v1/client/subscribe?token={$user['token']}";
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{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">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
}
}
}

View File

@ -4,22 +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
{
@ -40,7 +42,7 @@ class OrderController extends Controller
}
}
return response([
'data' => $order
'data' => $order->makeHidden(['id', 'user_id'])
]);
}
@ -50,189 +52,93 @@ class OrderController extends Controller
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('Order does not exist or has been paid'));
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)config('v2board.try_out_plan_id');
if (!$order['plan']) {
abort(500, '订阅不存在');
abort(500, __('Subscription plan does 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;
}
// surplus value
private function getSurplusValue(User $user)
{
$plan = Plan::find($user->plan_id);
switch ($plan->type) {
case 0: return $this->getSurplusValueByCycle($user, $plan);
case 1: return $this->getSurplusValueByOneTime($user, $plan);
}
}
private function getSurplusValueByOneTime(User $user, Plan $plan)
{
$trafficUnitPrice = 0;
$trafficUnitPrice = $plan->onetime_price / $plan->transfer_enable;
if ($user->discount && $trafficUnitPrice) {
$trafficUnitPrice = $trafficUnitPrice - ($trafficUnitPrice * $user->discount / 100);
}
$notUsedTrafficPrice = $plan->transfer_enable - (($user->u + $user->d) / 1073741824);
$result = $trafficUnitPrice * $notUsedTrafficPrice;
return $result > 0 ? $result : 0;
}
private function getSurplusValueByCycle(User $user, Plan $plan)
{
$dayPrice = 0;
if ($plan->month_price) {
$dayPrice = $plan->month_price / 2592000;
} else if ($plan->quarter_price) {
$dayPrice = $plan->quarter_price / 7862400;
} else if ($plan->half_year_price) {
$dayPrice = $plan->half_year_price / 15811200;
} else if ($plan->year_price) {
$dayPrice = $plan->year_price / 31536000;
}
// exclude discount
if ($user->discount && $dayPrice) {
$dayPrice = $dayPrice - ($dayPrice * $user->discount / 100);
}
$remainingDay = $user->expired_at - time();
$result = $remainingDay * $dayPrice;
return $result > 0 ? $result : 0;
}
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, __('You have an unpaid or pending order, please try again later or cancel it'));
}
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
abort(500, __('Subscription plan does not exist'));
}
if ($plan[$request->input('period')] === NULL) {
abort(500, __('This payment period cannot be purchased, please choose another period'));
}
if ($request->input('period') === 'reset_price') {
if ($user->expired_at <= time() || !$user->plan_id) {
abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
abort(500, '该订阅已售罄');
if ($request->input('period') !== 'reset_price') {
abort(500, __('This subscription has been sold out, please choose another subscription'));
}
}
if (!$plan->renew && $user->plan_id == $plan->id) {
abort(500, '该订阅无法续费,请更换其他订阅');
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
abort(500, __('This subscription cannot be renewed, please change to another subscription'));
}
if ($plan[$request->input('cycle')] === NULL) {
abort(500, '该订阅周期无法进行购买,请选择其他周期');
}
if ($request->input('coupon_code')) {
$coupon = Coupon::where('code', $request->input('coupon_code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
}
if (time() < $coupon->started_at) {
abort(500, '优惠券还未到可用时间');
}
if (time() > $coupon->ended_at) {
abort(500, '优惠券已过期');
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('This subscription has expired, please change to another subscription'));
}
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')];
// coupon start
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, '优惠券使用失败');
}
$order->period = $request->input('period');
$order->trade_no = Helper::generateOrderNo();
$order->total_amount = $plan[$request->input('period')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, __('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
// coupon complete
// discount start
if ($user->discount) {
$order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100));
}
// discount end
$order->total_amount = $order->total_amount - $order->discount_amount;
// renew and change subscribe process
if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
if (!(int)config('v2board.plan_change_enable', 1)) abort(500, '目前不允许更改订阅,请联系客服或提交工单');
$order->type = 3;
$order->surplus_amount = $this->getSurplusValue($user);
if ($order->surplus_amount >= $order->total_amount) {
$order->refund_amount = $order->surplus_amount - $order->total_amount;
$order->total_amount = 0;
} else {
$order->total_amount = $order->total_amount - $order->surplus_amount;
}
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = 2;
} else {
$order->type = 1;
}
// invite process
if ($user->invite_user_id && $order->total_amount > 0) {
$order->invite_user_id = $user->invite_user_id;
$commissionFirstTime = (int)config('v2board.commission_first_time_enable', 1);
if (!$commissionFirstTime || ($commissionFirstTime && !Order::where('user_id', $user->id)->where('status', 3)->first())) {
$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);
}
}
}
// use balance
$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, '余额不足');
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, '余额不足');
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
@ -241,7 +147,7 @@ class OrderController extends Controller
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
abort(500, __('Failed to create order'));
}
DB::commit();
@ -260,64 +166,31 @@ class OrderController extends Controller
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或已支付');
abort(500, __('Order does not exist or has been paid'));
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
exit();
}
switch ($method) {
// return type => 0: QRCode / 1: URL
case 0:
// alipayF2F
if (!(int)config('v2board.alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->alipayF2F($tradeNo, $order->total_amount)
]);
case 2:
// stripeAlipay
if (!(int)config('v2board.stripe_alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->stripeAlipay($order)
]);
case 3:
// stripeWepay
if (!(int)config('v2board.stripe_wepay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->stripeWepay($order)
]);
case 4:
// bitpayX
if (!(int)config('v2board.bitpayx_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->bitpayX($order)
]);
case 5:
if (!(int)config('v2board.paytaro_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->payTaro($order)
]);
default:
abort(500, '支付方式不存在');
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) abort(500, '');
return response([
'type' => -1,
'data' => true
]);
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('Payment method is not available'));
$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' => $result['type'],
'data' => $result['data']
]);
}
public function check(Request $request)
@ -327,7 +200,7 @@ class OrderController extends Controller
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('Order does not exist'));
}
return response([
'data' => $order->status
@ -336,191 +209,39 @@ 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',
'icon'
])
->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, __('Invalid parameter'));
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('Order does not exist'));
}
if ($order->status !== 0) {
abort(500, '只可以取消待支付订单');
abort(500, __('You can only cancel pending orders'));
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '取消失败');
abort(500, __('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)
{
$currency = config('stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'alipay',
'statement_descriptor' => $order->trade_no,
'metadata' => [
'user_id' => $order->user_id,
'invoice_id' => $order->trade_no,
'identifier' => ''
],
'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)
{
$currency = config('stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'wechat',
'metadata' => [
'user_id' => $order->user_id,
'invoice_id' => $order->trade_no,
'identifier' => ''
],
'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, __('Subscription plan does not exist'));
}
return response([
'data' => $plan
]);
}
$plan = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
return response([
'data' => $plan

View File

@ -3,10 +3,12 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerLog;
use App\Models\User;
@ -17,29 +19,14 @@ class ServerController extends Controller
public function fetch(Request $request)
{
$user = User::find($request->session()->get('id'));
$server = [];
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$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']);
}
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
}
return response([
'data' => $server
'data' => $servers
]);
}
@ -49,28 +36,23 @@ class ServerController extends Controller
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$serverLogModel = ServerLog::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC');
->orderBy('log_at', 'DESC');
switch ($type) {
case 0:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')));
break;
case 1:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-1')));
}
$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,27 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
public function getBotInfo()
{
$telegramService = new TelegramService();
$response = $telegramService->getMe();
return response([
'data' => [
'username' => $response->result->username
]
]);
}
public function unbind(Request $request)
{
$user = User::where('user_id', $request->session()->get('id'))->first();
}
}

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, __('Ticket does 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, __('There are other unresolved tickets'));
}
$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, __('Failed to open ticket'));
}
$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, __('Failed to open ticket'));
}
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, __('Invalid parameter'));
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
abort(500, __('Message cannot be empty'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('Ticket does not exist'));
}
if ($ticket->status) {
abort(500, '工单已关闭,无法回复');
abort(500, __('The ticket is closed and cannot be replied'));
}
if ($request->session()->get('id') == $this->getLastMessage($ticket->id)->user_id) {
abort(500, '请等待技术支持回复');
abort(500, __('Please wait for the technical enginneer to 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, __('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, __('Invalid parameter'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('Ticket does not exist'));
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
abort(500, __('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, __('Unsupported withdrawal method'));
}
$user = User::find($request->session()->get('id'));
$limit = config('v2board.commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
abort(500, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit]));
}
DB::beginTransaction();
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
$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, __('Failed to open ticket'));
}
$message = sprintf("%s\r\n%s",
__('Withdrawal method') . "" . $request->input('withdraw_method'),
__('Withdrawal 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, __('Failed to open ticket'));
}
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,77 +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', 'category_id', 'title'])
->where('show', 1)
->get()
->groupBy('category_id');
$user = User::find($request->session()->get('id'));
$response = [
'data' => [
'tutorials' => $tutorial,
'safe_area_var' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'],
'app_name' => config('v2board.app_name', 'V2board'),
'apple_id' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id', '本站暂无提供AppleID信息') : '账号过期或未订阅',
'apple_id_password' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id_password', '本站暂无提供AppleID信息') : '账号过期或未订阅'
]
]
];
// fuck support shadowrocket urlsafeb64 subscribe
$response['data']['safe_area_var']['b64_subscribe_url'] = str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($response['data']['safe_area_var']['subscribe_url'])
);
// end
return response($response);
}
}

View File

@ -3,15 +3,19 @@
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 App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Ticket;
use App\Utils\Helper;
use App\Models\Order;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
@ -23,26 +27,25 @@ 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, __('The user does not exist'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$request->input('old_password'),
$user->password)
) {
abort(500, '旧密码有误');
abort(500, __('The old password is wrong'));
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, '保存失败');
abort(500, __('Save failed'));
}
$request->session()->flush();
return response([
@ -59,7 +62,6 @@ class UserController extends Controller
'last_login_at',
'created_at',
'banned',
'is_admin',
'remind_expire',
'remind_traffic',
'expired_at',
@ -67,9 +69,14 @@ class UserController extends Controller
'commission_balance',
'plan_id',
'discount',
'commission_rate'
'commission_rate',
'telegram_id',
'uuid'
])
->first();
if (!$user) {
abort(500, __('The user does not exist'));
}
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return response([
'data' => $user
@ -95,14 +102,28 @@ 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([
'plan_id',
'token',
'expired_at',
'u',
'd',
'transfer_enable',
'email'
])
->first();
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, '订阅计划不存在');
abort(500, __('Subscription plan does not exist'));
}
}
$user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$user['subscribe_url'] = Helper::getSubscribeHost() . "/api/v1/client/subscribe?token={$user['token']}";
$user['reset_day'] = $this->getResetDay($user);
return response([
'data' => $user
]);
@ -111,10 +132,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, __('The user does not exist'));
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, '重置失败');
abort(500, __('Reset failed'));
}
return response([
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
@ -130,16 +154,82 @@ class UserController extends Controller
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
abort(500, __('The user does not exist'));
}
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
abort(500, __('Save failed'));
}
return response([
'data' => true
]);
}
public function transfer(UserTransfer $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, __('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, __('Transfer failed'));
}
return response([
'data' => true
]);
}
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
// if reset method is not reset
if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
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;
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('The user does not exist'));
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$url = config('v2board.app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
}

View File

@ -35,6 +35,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CORS::class,
],
'api' => [
@ -43,7 +44,7 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\CORS::class,
'throttle:120,1',
\App\Http\Middleware\Language::class,
'bindings',
],
];
@ -68,7 +69,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,12 +15,16 @@ 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 ($user) {
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if ($authorization) {
$authData = explode(':', base64_decode($authorization));
if (!isset($authData[1]) || !isset($authData[0])) abort(403, '鉴权失败,请重新登入');
$user = \App\Models\User::where('password', $authData[1])
->where('email', $authData[0])
->first();
if (!$user) abort(403, '鉴权失败,请重新登入');
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
if (!$request->session()->get('id')) {
abort(403, '未登录或登陆已过期');

View File

@ -6,64 +6,6 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest
{
CONST RULES = [
// 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',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable|url',
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'renew_reset_traffic_enable' => 'in:0,1',
// server
'server_token' => 'nullable|min:16',
'server_license' => 'nullable',
// alipay
'alipay_enable' => 'in:0,1',
'alipay_appid' => 'nullable|integer|min:16',
'alipay_pubkey' => 'max:2048',
'alipay_privkey' => 'max:2048',
// stripe
'stripe_alipay_enable' => 'in:0,1',
'stripe_wepay_enable' => 'in:0,1',
'stripe_sk_live' => '',
'stripe_pk_live' => '',
'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd',
// bitpayx
'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '',
// paytaro
'paytaro_enable' => 'in:0,1',
'paytaro_app_id' => '',
'paytaro_app_secret' => '',
// frontend
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_background_url' => 'nullable|url',
// tutorial
'apple_id' => 'email',
'apple_id_password' => '',
// email
'email_template' => ''
];
/**
* Get the validation rules that apply to the request.
*
@ -71,7 +13,114 @@ class ConfigSave extends FormRequest
*/
public function rules()
{
return self::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',
'commission_distribution_enable' => 'in:0,1',
'commission_distribution_l1' => 'nullable|numeric',
'commission_distribution_l2' => 'nullable|numeric',
'commission_distribution_l3' => 'nullable|numeric',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable',
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'email_whitelist_enable' => 'in:0,1',
'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',
'currency' => '',
'currency_symbol' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2',
'surplus_enable' => 'in:0,1',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
'change_order_event_id' => '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',
'alipay_pubkey' => 'max:2048',
'alipay_privkey' => 'max:2048',
// stripe
'stripe_alipay_enable' => 'in:0,1',
'stripe_wepay_enable' => 'in:0,1',
'stripe_card_enable' => 'in:0,1',
'stripe_sk_live' => '',
'stripe_pk_live' => '',
'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
// bitpayx
'bitpayx_name' => '',
'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '',
// mGate
'mgate_name' => '',
'mgate_enable' => 'in:0,1',
'mgate_url' => 'nullable|url',
'mgate_app_id' => '',
'mgate_app_secret' => '',
// Epay
'epay_name' => '',
'epay_enable' => 'in:0,1',
'epay_url' => 'nullable|url',
'epay_pid' => '',
'epay_key' => '',
// frontend
'frontend_theme' => '',
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black,green',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
'frontend_customer_service_method' => '',
'frontend_customer_service_id' => '',
// 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' => '',
'telegram_discuss_link' => 'nullable|url',
// app
'windows_version' => '',
'windows_download_url' => '',
'macos_version' => '',
'macos_download_url' => '',
'android_version' => '',
'android_download_url' => ''
];
}
public function messages()
@ -79,7 +128,10 @@ class ConfigSave extends FormRequest
// illiteracy prompt
return [
'app_url.url' => '站点URL格式不正确必须携带http(s)://',
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://'
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://',
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://',
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class CouponSave extends FormRequest
class CouponGenerate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,18 +14,25 @@ class CouponSave extends FormRequest
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_use' => 'nullable|integer',
'limit_use_with_user' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'limit_period' => 'nullable|array',
'code' => ''
];
}
public function messages()
{
return [
'generate_count.integer' => '生成数量必须为数字',
'generate_count.max' => '生成数量最大为500个',
'name.required' => '名称不能为空',
'type.required' => '类型不能为空',
'type.in' => '类型格式有误',
@ -35,7 +42,10 @@ class CouponSave extends FormRequest
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误'
'limit_use.integer' => '最大使用次数格式有误',
'limit_use_with_user.integer' => '限制用户使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误',
'limit_period.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

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class KnowledgeSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'category' => 'required',
'language' => 'required',
'title' => 'required',
'body' => 'required'
];
}
public function messages()
{
return [
'title.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

@ -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',
'period' => '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' => '支付金额不能为空',
'period.required' => '订阅周期不能为空',
'period.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,callback_no',
'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,2'
'commission_status' => 'in:0,1,3'
];
}

View File

@ -6,17 +6,6 @@ use Illuminate\Foundation\Http\FormRequest;
class PlanSave extends FormRequest
{
CONST RULES = [
'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',
'onetime_price' => 'nullable|integer'
];
/**
* Get the validation rules that apply to the request.
*
@ -24,7 +13,21 @@ class PlanSave extends FormRequest
*/
public function rules()
{
return self::RULES;
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',
'two_year_price' => 'nullable|integer',
'three_year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer',
'reset_traffic_method' => 'nullable|integer|in:0,1,2'
];
}
public function messages()
@ -39,7 +42,12 @@ class PlanSave extends FormRequest
'quarter_price.integer' => '季付金额格式有误',
'half_year_price.integer' => '半年付金额格式有误',
'year_price.integer' => '年付金额格式有误',
'onetime_price.integer' => '一次性金额有误'
'two_year_price.integer' => '两年付金额格式有误',
'three_year_price.integer' => '三年付金额格式有误',
'onetime_price.integer' => '一次性金额有误',
'reset_price.integer' => '流量重置包金额有误',
'reset_traffic_method.integer' => '流量重置方式格式有误',
'reset_traffic_method.in' => '流量重置方式格式有误'
];
}
}

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

@ -1,53 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends FormRequest
{
CONST RULES = [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic',
'networkSettings' => '',
'ruleSettings' => '',
'tlsSettings' => ''
];
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return self::RULES;
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'parent_id.integer' => '父ID格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'tls.required' => 'TLS不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'network.required' => '传输协议不能为空',
'network.in' => '传输协议格式不正确'
];
}
}

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

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerV2raySave 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',
'tls' => 'required',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'alter_id' => 'required|integer',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic,grpc',
'networkSettings' => 'nullable|array',
'ruleSettings' => 'nullable|array',
'tlsSettings' => 'nullable|array',
'dnsSettings' => 'nullable|array'
];
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'parent_id.integer' => '父ID格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'tls.required' => 'TLS不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'network.required' => '传输协议不能为空',
'network.in' => '传输协议格式不正确',
'networkSettings.array' => '传输协议配置有误',
'ruleSettings.array' => '规则配置有误',
'tlsSettings.array' => 'tls配置有误',
'dnsSettings.array' => 'dns配置有误'
];
}
}

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

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

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

@ -6,21 +6,6 @@ use Illuminate\Foundation\Http\FormRequest;
class UserUpdate extends FormRequest
{
CONST RULES = [
'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',
'is_admin' => 'required|in:0,1',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer'
];
/**
* Get the validation rules that apply to the request.
*
@ -28,7 +13,24 @@ class UserUpdate extends FormRequest
*/
public function rules()
{
return self::RULES;
return [
'email' => 'required|email',
'password' => 'nullable|min:8',
'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',
'is_admin' => 'required|in:0,1',
'is_staff' => 'required|in:0,1',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_type' => 'integer',
'commission_balance' => 'integer',
'remarks' => 'nullable'
];
}
public function messages()
@ -42,6 +44,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' => '推荐返利比例格式不正确',
@ -54,7 +58,8 @@ class UserUpdate extends FormRequest
'u.integer' => '上行流量格式不正确',
'd.integer' => '下行流量格式不正确',
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确'
'commission_balance.integer' => '佣金格式不正确',
'password.min' => '密码长度最小8位'
];
}
}

View File

@ -23,11 +23,11 @@ class AuthForget extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数',
'email_code.required' => '邮箱验证码不能为空'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits'),
'email_code.required' => __('Email verification code cannot be empty')
];
}
}

View File

@ -22,10 +22,10 @@ class AuthLogin extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits')
];
}
}

View File

@ -22,10 +22,10 @@ class AuthRegister extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits')
];
}
}

View File

@ -21,8 +21,8 @@ class CommSendEmailVerify extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect')
];
}
}

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