357 Commits

Author SHA1 Message Date
Micah123321 66f34d7514 Merge branch 'cedar2025:dev' into dev 2026-02-22 03:20:08 +08:00
Xboard cab76f1cf3 Update default.conf 2025-07-10 19:05:29 +08:00
Xboard 99413c5962 Update aapanel安装指南.md 2025-02-06 14:04:37 +08:00
Xboard d231b6ebcd Update aapanel+docker安装指南.md 2025-01-25 17:07:31 +08:00
xboard 6c67cbf85a update docs 2025-01-22 21:16:51 +08:00
Xboard 7acae3dcc4 Update docker-publish.yml 2025-01-21 22:14:01 +08:00
Xboard dff2e721cb Update docker-publish.yml 2025-01-21 22:05:56 +08:00
Xboard 3a05281a9f Update MigrateFromV2b.php 2025-01-18 21:57:33 +08:00
Xboard 74d93691b9 Merge pull request #313 from elysias123/dev
allow admin/staff unbind a user and better subscription domain names
2025-01-17 11:59:11 +08:00
Xboard d743296392 Merge pull request #325 from rebecca554owen/patch-1
Sync
2025-01-16 23:01:32 +08:00
rebecca554owen 024a9dfb54 fix update.sh 2025-01-16 22:44:10 +08:00
Xboard d3bc8ff8fe Merge pull request #192 from yushum/patch-1
Update websocket settings
2025-01-16 12:52:30 +08:00
Xboard ea6a2d8fc6 Merge pull request #317 from linusxiong/patch-1
修复Stripe支付方式中导致400的小Bug
2025-01-16 12:48:58 +08:00
xboard f6af7313d0 update docs 2025-01-16 10:13:53 +08:00
xboard 4877866fe8 update docs 2025-01-16 09:58:04 +08:00
xboard 18c90a0aee update docker-publish.yml 2025-01-16 09:50:45 +08:00
大大白 4d2b442885 Update StripeALLInOne.php
fix a bug that causes 400: https://support.stripe.com/questions/use-of-the-statement-descriptor-parameter-on-paymentintents-for-card-charges
2025-01-14 14:43:52 -05:00
Elysia a550fd1436 feat: Replace with a random string when the subscription domain contains *&Replace with the user uuid when the subscription domain contains {uuid} 2025-01-14 13:54:26 +08:00
Elysia 43bac89d3a Merge branch 'cedar2025:dev' into dev 2025-01-13 21:35:45 +08:00
xboard d57c3ca60d Revert "update docs"
This reverts commit 558834b154.
2025-01-13 07:57:07 +08:00
Xboard 558834b154 update docs 2025-01-13 07:55:19 +08:00
Elysia e2262f1435 feat: allow admin/staff unbind a user 2025-01-12 15:31:51 +08:00
Xboard a60b23b17d Merge pull request #280 from longdoer/patch-1
Update ClashMeta.php
2025-01-10 13:37:11 +08:00
Xboard 6aee3ea40e Merge pull request #295 from sechk777/patch-1
Update default.sing-box.json
2025-01-10 13:36:28 +08:00
Xboard 89d2aed46d Revert "Update 1panel安装指南.md"
This reverts commit ae249f8e96.
2025-01-09 23:57:38 +00:00
xboard cf44a62db7 Revert "Update 1panel安装指南.md"
This reverts commit cddcb144ef.
2025-01-09 10:15:24 +08:00
xboard 5b3474a06d Revert "update docker-compose安装指南.md"
This reverts commit dde41da139.
2025-01-09 10:15:21 +08:00
Xboard dde41da139 update docker-compose安装指南.md 2025-01-08 10:43:06 -05:00
Xboard cddcb144ef Update 1panel安装指南.md 2025-01-07 17:14:46 -05:00
Xboard ae249f8e96 Update 1panel安装指南.md 2025-01-07 17:14:07 -05:00
Xboard 9bc1d7a286 Merge pull request #136 from linusxiong/dev
[update] 新增stripe聚合支付方式,采用全新的paymentIntents API
2025-01-07 10:56:12 +08:00
大大白 256ca28e00 Merge branch 'cedar2025:dev' into dev 2024-12-31 13:36:02 -05:00
seck d6b7ae6404 Update default.sing-box.json
The inet4_address and inet6_address types have been consolidated into a unified address type, in alignment with the official documentation.
2024-12-19 15:34:58 +08:00
Xboard de18cfe596 fix: resolve hy2 speed limit dispatch issue in Singbox client 2024-12-09 18:45:12 +08:00
大大白 654f1f84fb Update supervisord.conf 2024-12-08 04:58:38 -05:00
Linus Xiong aa3ff5cb66 fix bug 2024-12-08 01:48:49 -05:00
大大白 22ffe0dace Update Dockerfile 2024-12-08 01:38:27 -05:00
大大白 dee7525bb4 Update Dockerfile 2024-12-08 01:10:00 -05:00
Linus Xiong cfc8a05cba fix bugs 2024-12-08 01:05:38 -05:00
Linus Xiong 1378fdb45b fix bugs 2024-12-08 00:58:58 -05:00
Linus Xiong c4595bc665 use docker env on .env file 2024-12-08 00:53:43 -05:00
大大白 1d9cb2295c Merge branch 'cedar2025:dev' into dev 2024-12-01 21:04:35 -05:00
xboard f86ccae28c pref: Enhance TrafficFetch Performance and Optimize Code Structure 2024-11-29 19:22:12 +08:00
longdoer ddac216e2d Update ClashMeta.php
增加ClashMeta首页链接
2024-11-19 16:45:52 +08:00
Xboard 0e9739af0b Update composer.json 2024-11-17 17:02:07 +08:00
xboard db6a361857 Revert "chore: update composer.json"
This reverts commit dd246b2c16.
2024-11-15 15:10:05 +08:00
大大白 e482b72430 Merge branch 'cedar2025:dev' into dev 2024-11-14 17:05:23 -05:00
xboard 090781db0c feat: add support for hysteria2 in V2rayNG 2024-11-09 22:16:05 +08:00
xboard e79465c90d feat: add support for hysteria2 in Passwall and SSRPLUS 2024-11-09 21:37:15 +08:00
xboard ab52e61ed1 refactor: subscription URL retrieval logic 2024-11-09 06:35:37 +08:00
Xboard 73b25d2eec Update aapanel+docker安装指南.md 2024-11-09 04:13:45 +08:00
Xboard b5ea28cd27 Update docker-compose安装指南.md 2024-11-09 04:01:39 +08:00
Xboard fd37291716 feat: add appname display in hiddify
add profile-title header to sing-box subscribe
2024-11-08 17:26:31 +08:00
Xboard 2251f218d8 chore: add persistence configuration to redis 2024-11-08 06:32:48 +08:00
xboard dd246b2c16 chore: update composer.json 2024-11-05 11:56:02 +08:00
Xboard 6ab033c4cf 合并来自greatbody/dev的拉取请求#215
feat: Support configure environment variable to skip interactive configurat…
2024-11-02 14:30:54 +08:00
Xboard 421a0c36ed Merge pull request #238 from mercury7720/dev
[feat]: add v2rayNG hysteria2 support
2024-11-02 14:25:51 +08:00
Linux f25696f22b Payment: add type configuration to Epay payment 2024-10-20 12:37:16 +08:00
Linux 4d4adc4fac fix: Hide online user count display for child nodes without independent online users 2024-10-20 12:03:21 +08:00
Linux c7ea56bb7e docs: update 2024-10-19 23:17:22 +08:00
大大白 6cfdd1c9b1 Update Surge.php
support surge ss2022
2024-10-18 12:42:51 -04:00
大大白 1d36069726 Merge branch 'cedar2025:dev' into dev 2024-10-16 19:37:04 -04:00
George 8737e0a6eb Merge remote-tracking branch 'upstream/dev' into feature/v2rayng-hysteria2 2024-10-10 11:18:20 +08:00
Linux f5746423bf fix: 修复用户端不显示当日流量的问题 2024-10-09 22:55:26 +08:00
George 0873ce591f [feat]: add v2rayNG hysteria2 support 2024-10-09 15:17:45 +08:00
xboard af1f1e9fdf fix: 修复流量明细显示所有记录的BUG 2024-10-07 09:55:19 +08:00
Xboard 2f29c5f118 Merge pull request #227 from mercury7720/dev
[fix]: Loon Hysteria2 node cannot get SNI
2024-10-07 05:48:29 +08:00
xboard 47b0646811 fix: 修复vless节点的tls_insecure在meta中下发生效的问题 2024-10-07 02:47:54 +08:00
xboard a7f42a6459 fix: 修复节点密钥未校验是否为空的问题 2024-10-07 00:27:11 +08:00
admin 46656fe4f7 Update Server.php 2024-10-06 01:24:41 +08:00
George 0e2d961902 [fix]: Loon Hysteria2 node cannot get SNI 2024-09-29 11:28:18 +08:00
大大白 190e64b7c2 Merge branch 'cedar2025:dev' into dev 2024-09-16 00:09:09 -04:00
greatbody 9517b0144f Enable mannual trigger of pipeline. 2024-09-06 10:08:17 +08:00
greatbody 0fb61ce30e Support configure environment variable to skip interactive configuration, which is require when we want to setup by script. 2024-09-06 10:02:45 +08:00
xboard 912cb397ea fix: [Xboard主题] 修复扫描二维码订阅中二维码显示位置歪了的问题 2024-08-27 13:55:20 +08:00
Yusum 42542725f7 Update Shadowrocket.php 2024-08-01 21:56:49 +08:00
Yusum ab34ef327a Update SingBox.php 2024-08-01 21:21:36 +08:00
Yusum 60ed240e66 Update ClashMeta.php 2024-08-01 21:16:56 +08:00
xboard cf13c87873 update docker-publish.yml 2024-07-29 13:09:25 +08:00
xboard 2f9d28beb0 fix: [Xboard主题] 修复订单金额计算的问题 2024-07-24 00:25:37 +08:00
Xboard 5f3b95b699 Merge pull request #180 from ishkong/patch-1
feat: 支持部分客户端订阅获取shadowsocks中obfs部分参数
2024-07-21 17:48:17 +08:00
ishkong b8c2197e89 feat: Support V2RayNG to get obfs parameters in shadowsocks. 2024-07-21 17:29:12 +08:00
ishkong e46f2b3390 feat: Support shadowrocket to get obfs parameters in shadowsocks. 2024-07-21 14:01:03 +08:00
ishkong dd8267097e feat: Support Shadowsocks obfs config 2024-07-21 13:58:22 +08:00
大大白 b0fcc9244a Merge branch 'cedar2025:dev' into dev 2024-07-20 04:11:24 +08:00
xboard c7b15e6b8b fix:【Xboard主题】 修复订单金额计算在分配订单情况下显示有误的问题, 修改部分弹窗的按钮颜色与主题色一致 2024-07-20 02:53:06 +08:00
xboard c18f55d6dd fix: 修复上一个commit造成的bug 2024-07-19 07:04:02 +08:00
xboard 163d09c71b feat: Singbox、clash订阅增加绕过服务器地址规则
1、优化订阅下发代码
2、singbox、clash、meta订阅下发增加绕过服务器地址规则,防止重复代理
2024-07-19 06:29:35 +08:00
xboard 3f264c17ba update default.sing-box.json 2024-07-19 02:33:27 +08:00
xboard 6a32cf28fb fix: Fix OrderHandleJob to prevent Lock wait timeout. 2024-07-19 02:30:51 +08:00
Xboard f9e4a978fd Update aapanel+docker安装指南.md 2024-07-16 02:25:49 +08:00
xboard 14a2a09d7b fix:[Xboard主题] 修复支付弹窗二维码溢出盒子的问题 2024-07-04 13:36:25 +08:00
LinusX acb40cc1f9 [update] 新增credit card跳转至checkout页面付款 2024-07-02 19:02:29 +08:00
LinusX f0c620cbc2 [fix] 删除重复包导入 2024-06-17 22:58:53 +08:00
LinusX 8cc247b653 [feat] 新增telegram机器人/start指令 2024-06-15 02:16:54 +08:00
LinusX bab7ed8e97 [fix] 修复catch的时候变量可能undefined的问题 2024-06-15 01:42:43 +08:00
大大白 0389edd4d0 Merge branch 'cedar2025:dev' into dev 2024-06-15 01:05:54 +08:00
xboard f099cf6d30 xboard主题: 公告增加自动轮播 2024-06-14 01:43:55 +08:00
xboard 6cc719ebf5 fix: 修复59f40df 导致的订单tg通知失败的问题 2024-06-14 00:05:20 +08:00
大大白 338aad7f6c Fix PaymentController.php
修复由Jun 8, 2024提交的2个commit产生的报错
2024-06-13 23:25:23 +08:00
大大白 fc283af60f Update StripeALLInOne.php
删除调试冗余代码
2024-06-13 18:47:04 +08:00
大大白 9270d94668 Update StripeALLInOne.php
修复部分报错语句不规范
2024-06-13 18:40:40 +08:00
大大白 5cc0b77982 Merge branch 'cedar2025:dev' into dev 2024-06-13 18:39:03 +08:00
xboard 81be6f375b update default.sing-box.json
migrate singbox configuration to v1.9.0
2024-06-10 12:27:13 +08:00
Xboard cd1bd1109c Merge pull request #145 from riolurs/patch-1
feat: Add additional payment information to Telegram notifications.
2024-06-08 21:41:32 +08:00
riolu.rs cd45e55620 详细支付信息 2024-06-08 17:51:38 +08:00
LinusX 261487437b [update] 新增stripe聚合支付方式,采用全新的paymentIntents API
[fix] 修改支付方式中的小bug
[update] 将stripe-php版本升级至最新
2024-06-05 07:55:49 +08:00
xboard 59f40dfd02 update feature-request---功能请求.md 2024-06-02 22:23:39 +08:00
xboard 8416381fa0 fix: 修复支付完成后的返回URL可能不为来源站点的问题 2024-06-02 22:22:57 +08:00
Xboard 72fb7031e5 Update aapanel安装指南.md 2024-06-02 13:56:57 +08:00
xboard 2c5f610ed0 fix: 修复今日节点实时流量排行超过15个排序变乱的问题 2024-05-31 14:39:16 +08:00
xboard 7246eb6ebc fix: 修复初始化安装时清理缓存报错的问题
修改aapanel 安装文档增加inotify 扩展说明
修改webman.php 自动检测是否安装inotify扩展,如果安装则自动拉取热重载监控
2024-05-29 00:56:00 +08:00
xboard fd52795f49 fix: 修复ss2022订阅下发密码错误的问题 2024-05-24 22:45:27 +08:00
xboard 2b0bf6cbb7 perf: 优化安装体验 2024-05-24 13:06:25 +08:00
xboard 033b8c702a hy2: 不带版本号的客户端默认下发hy2节点 2024-05-23 16:42:57 +08:00
xboard dd78dbde5c feat: workerman 增加热重载 2024-05-23 11:40:52 +08:00
xboard 1cfd077ae7 log: 修改日志导出结果路径提示为绝对路径 2024-05-20 10:22:31 +08:00
xboard 9b729aa79b update docs 2024-05-18 00:23:00 +08:00
xboard ef914283aa to: 追加sing-box下发忘记提交的代码 2024-05-15 01:06:41 +08:00
xboard d1eef94e6b fix: 修复上一个commit导致的shadowrocket的hy2节点订阅下发失败的问题 2024-05-15 01:05:27 +08:00
xboard 5a0e59b103 feat: 增加surge的hy2下发、添加clash meta、shadowrocket、stash订阅hy2端口跳跃的下发 2024-05-14 21:57:36 +08:00
xboard 227f50b9d1 chore: 优化docker环境下的启动速度,增加webman进程自动重启来释放内存 2024-05-14 18:10:15 +08:00
xboard fa6dfc6acb fix: 修复Surge surfboard订阅中的剩余流量显示错误的问题 2024-05-03 15:04:23 +08:00
xboard 365994b1af update docs 2024-05-03 03:36:29 +08:00
xboard 201f230e80 chore: 修复docker 环境当中,因为缺少patch导致打patch失败的问题 2024-05-03 03:35:47 +08:00
xboard 42d2df07a7 fix: 修复订阅过滤功能在某些情况下报错的问题 2024-04-30 11:02:21 +08:00
xboard f9c12d7bd8 to: 追加上个commit 缺少提交的文件 2024-04-27 21:22:45 +08:00
xboard be9ed269fa perf: 优化流量消费相关代码性能,解决流量纪录与已用流量有概率不一致的问题 2024-04-27 17:06:57 +08:00
xboard 4438fee3ca fix:[Xboard主题] 修复重置密码页面不显示邮箱白名单后缀的问题 2024-04-24 22:45:03 +08:00
xboard 2c8b06dc7b fix:[Xboard主题]修复重构导致的注册邮箱白名单不显示的问题 2024-04-21 20:09:14 +08:00
xboard ce31a604b2 update docs 2024-04-21 02:17:19 +08:00
xboard cbd7c91d26 [Xboard主题]: 当订阅停售且用户续费订阅时跳转到订阅列表页 2024-04-21 01:51:05 +08:00
xboard 285b240dec refactor:[Xboard主题] 重构登陆、注册、忘记密码页面
修复用户注册时第二次提交recaptcha不弹出的问题
修改Info弹窗颜色为主题色
2024-04-21 01:39:53 +08:00
xboard a0aa6d25d4 update docs 2024-04-20 20:27:45 +08:00
xboard 09fb03a1ce fix: 修复当 server_token 为长数字时,后台会转化成科学计数法,后端验证失败 2024-04-18 01:27:52 +08:00
xboard f72df9df27 chore: 优化docker 容器启动速度 2024-04-15 03:19:48 +08:00
xboard 5659bd96f2 docs: 增加1panel部署文档 2024-04-15 02:41:03 +08:00
xboard 2eb81924e4 fix: 修复节点密钥包含特殊字符时校验失败的问题 2024-04-15 00:01:14 +08:00
xboard 0c2360972b chore: 在 Docker 启动时自动修改项目目录权限 2024-04-13 04:10:47 +08:00
xboard 93200ac057 fix: 修复无法读取未携带Content-Type的请求头的节点后端上报的数据的问题 2024-04-13 03:23:30 +08:00
xboard c2b08c2627 fix: 修复更新后导致节点后端交互api不可用的问题 2024-04-12 17:34:06 +08:00
xboard 7eb8d76c87 chore: 修复docker环境该用非root运行时全挂载和sqlite部署报错并减少layers 2024-04-12 16:37:21 +08:00
xboard 03706c054d chore: docker环境修改php为非root运行 2024-04-11 20:37:46 +08:00
xboard 39b4218349 fix: 修复安装Command 2024-04-10 19:09:16 +08:00
xboard 4c6c7182e2 refactor: 重构规范部分代码、邮件队列增加失败重试、去除多个支付方式、更新依赖 2024-04-10 00:51:03 +08:00
Xboard ec63e05575 Update webman.php 2024-04-09 22:52:10 +08:00
xboard bb708d6084 chore: 增加通过环境变量WEBMAN_WORKERS修改Webman的worker数的功能 2024-04-07 22:23:34 +08:00
xboard 2ee630e94c fix: 修复早期安装用户更新后访问首页会报theme_color不存在的问题 2024-04-05 06:51:59 +08:00
xboard aeb39e1476 fix: 修复Xboard切换主题色不生效的问题 2024-04-04 18:04:21 +08:00
xboard 723173426b fix:[Xboard主题] 修复支付手续费部分情况下计算与实际不符的问题与支付图标布局的问题 2024-04-04 17:01:47 +08:00
xboard 44567e552a chore: docker构造环境增加bcmath扩展 2024-04-03 11:00:39 +08:00
xboard 1e503f444b fix: 调整支付通道百分比手续费可为0 2024-04-03 10:33:40 +08:00
xboard a7709d06d1 feat: 更改v2_payment表icon字段为text类型,增加图标对base64输入的支持 2024-04-03 07:29:24 +08:00
xboard d1470bb19d fix:[Xboard主题] 进行了各种改进和修复
1、去除未使用的图片
2、修改Google验证码reCaptcha地址为www.recaptcha.net
3、修复部分页面返回最顶部按钮被挡住的问题
4、补全部分缺失的语言包
5、修复部分分辨率下侧边栏菜单切换后不会自动收起的问题
6、修复暗黑模式下登陆注册忘记密码页面为白色背景的问题
7、调整手机端弹窗增加与屏幕边界的间隙
8、当有未完成订单的情况下,增加首页重置流量的弹窗提示
9、修复节点列表在手机端上显示不协调的问题
10、去除工单ID的显示
2024-04-02 14:45:54 +08:00
Xboard 614d771b84 Update docker-publish.yml 2024-03-20 19:41:45 +08:00
xboard dd6388ca03 update: docker-publish.yml 2024-03-20 19:29:16 +08:00
xboard 6c10f8dcb2 docs: update docs 2024-03-20 18:47:28 +08:00
xboard 00ea1b898a fix:【Xboard主题】修复语言包与路由跳转相关的问题
修复token2Login在已登陆情况下无法跳转参数指定路由的问题
    修复登陆成功后无法跳转指定页面的问题
    修复前端语言与后台返回语言不通不的问题
    调整邀请码列表样式(增加边框)
2024-03-20 02:24:42 +08:00
xboard 120a54279f docs: update docs 2024-03-13 05:23:35 +08:00
xboard 9241369cbb chore: docker添加ipv6的监听 2024-03-09 05:56:58 +08:00
xboard 9455d9ed56 feat: [Xboard主题]增加token2Login 2024-03-07 23:39:26 +08:00
xboard d63dc106b3 fix:[Xboard主题] 修复当支付金额为0时固定手续费显示不为0的问题、修复谷歌人机认证无效的问题 2024-03-07 04:36:42 +08:00
xboard d69517b11e docs: update docs 2024-02-28 18:34:27 +08:00
xboard 53efe20a86 fix: 修复shadowrocket grpc协议sni下发错误的问题 2024-01-24 20:26:58 +08:00
xboard 2e08328ec1 update README.md 2024-01-22 08:44:59 +08:00
xboard d0aa8b47af docs: 删除多余的readme.md 2024-01-22 08:40:53 +08:00
Xboard 40726f1405 Merge pull request #58 from rebecca554owen/dev
delete apanel docs
2024-01-18 23:36:12 +08:00
rebecca554owen a51da2059b Update README.md 2024-01-18 21:19:05 +08:00
rebecca554owen eee367c5a0 Delete docs 2024-01-18 21:18:28 +08:00
xboard 9939f2c2e1 fix: 修复vless 使用Tls时sni不下发的问题 2024-01-17 17:54:33 +08:00
xboard 3a4efcc1f8 fix: 修复vless节点使用tls时订阅抱错的问题 2024-01-16 17:02:42 +08:00
xboard f610f45523 perf:[Xboard主题] 一些样式调整
1、个人中心绑定TG按钮增加已经绑定判断、
    2、侧边栏菜单增加自动收起手机端自动切换为抽屉调整邀请页面佣金和邀请码布局
    3、修改订阅列表为左对齐
2024-01-15 05:51:16 +08:00
xboard 68d7d64ea7 fix: 修复一些主题用户获取节点为空的问题,取消节点API未找到节点的日志记录 2024-01-15 04:25:25 +08:00
xboard 8e57bc4e9b fix: 修复一些主题节点状态显示有误的问题 2024-01-13 17:55:28 +08:00
Xboard caa22ae9d2 Merge pull request #55 from ishkong/patch-2
feat: 增加v2rayN自动下发Hysteria2节点
2024-01-13 17:46:30 +08:00
Xboard e67d0f7d87 Merge pull request #52 from ishkong/patch-1
feat: 为v2rayn的订阅添加hysteria2支持
2024-01-13 17:46:08 +08:00
ishkong 0b5baa70f0 Update V2rayN.php 2024-01-13 17:25:11 +08:00
ishkong 9ed3cc5a74 feat: 增加v2rayN自动下发Hysteria2节点 2024-01-13 17:16:22 +08:00
Xboard 03267b2051 Merge pull request #54 from sakurauidev/ResetTraffic
将周期重置流量只重置已使用流量修改为重置已使用流量的同时重置当总流量为当前订阅所设置的流量
2024-01-13 12:11:14 +08:00
ventle233 3611ed6c88 将重置流量只重置已使用流量修改为重置已使用流量的同时重置当总流量为当前订阅所设置的流量 2024-01-13 02:10:15 +08:00
xboard 07b3a8dfd8 fix: [Xboard主题] 修改谷歌recaptcha为中国可以访问地址,解决中国用户无法打开进行人机认证的问题 2024-01-12 21:16:42 +08:00
ishkong 0000c071e7 为v2rayn的订阅添加hysteria2支持 2024-01-11 22:15:43 +08:00
xboard aa63664892 docs: update aapanel安装指南.md 2024-01-11 21:47:38 +08:00
xboard 3f65ad6ae6 to: 增加支付回调错误日志记录 2024-01-11 01:16:10 +08:00
xboard 31b8930222 style: 去除v2board 字段css 改为xborad 尝试解决被墙问题 2024-01-11 00:22:29 +08:00
xboard ec18ca0ee1 fix: [用户前端]修复更改订阅提示框显示条件有误的问题 2024-01-11 00:11:54 +08:00
xboard 3273274369 fix: 修复升级后不选择主题色会导致500错误的问题 2024-01-10 20:00:13 +08:00
xboard 95675de30b feat: [用户前端] 修复部分BUG、增加主题色、增加注册用户协议显示等 2024-01-10 18:59:59 +08:00
xboard 11591b9708 chore: nginx增加html文件访问支持 2024-01-06 14:54:39 +08:00
xboard ca1758dc48 feat: 增加Command (php artisan log:export)导出日志命令 2024-01-01 04:52:49 +08:00
xboard 59c21a9c8d feat: 增加邮件Mailgun支持 2024-01-01 04:40:23 +08:00
xboard cdc86bbe47 fix: 修复邮箱配置无法从环境变量读取的问题 2024-01-01 01:06:07 +08:00
Xboard 2c963d9a7d Merge pull request #35 from rebecca554owen/dev
v2baord迁移教程有误,重新修改。
2023-12-24 21:28:58 +08:00
rebecca554owen b8d9221d9b Update 从aapanel迁移到1panel教程.md 2023-12-19 23:18:20 +08:00
rebecca554owen 50c80614d1 Update 从aapanel迁移到1panel教程.md 2023-12-19 23:17:17 +08:00
rebecca554owen 1ae1767063 Update 从aapanel迁移到1panel教程.md 2023-12-19 23:03:54 +08:00
xboard 41643a729a perf: 优化初始化安装流程,增加mysql和Redis配置校验 2023-12-18 17:42:59 +08:00
xboard 9dcb9cee32 feat: 增加Vmess tcp http下发支持 2023-12-18 09:40:35 +08:00
xboard 0ef19e91bb fix:[用户前端] 修复扫描二维码订阅地址错误的问题 2023-12-17 21:56:05 +08:00
Xboard 0b58ac5779 Merge pull request #30 from rebecca554owen/dev
更新1paenl教程,删除多开教程,更新docker生成,方便fork分支用户自行修改以及测试。
2023-12-16 20:10:00 +08:00
rebecca554owen c5f23fbd12 多开Xboard教程存在Redis共用处理问题,先删除。 2023-12-16 18:09:19 +08:00
rebecca554owen 5e9707cf1a 更新教程适用于新安装,v2board/xboard用户迁移。 2023-12-16 18:08:33 +08:00
rebecca554owen 9f1f041494 修改生成镜像为仓库拥有者。 2023-12-16 18:07:32 +08:00
xboard 44eb05fb5b fix: 修复v2ray、Trojan旧版接口获取用户列表失败的问题 2023-12-16 08:05:09 +08:00
xboard 92532333cb fix: 修复节点状态列表接口报错的问题 2023-12-15 22:46:09 +08:00
xboard b934128449 fix: 修复Xboard主题扫描二维码不显示节点类型的问题 2023-12-14 22:29:37 +08:00
xboard 7bac88d593 fix: 修复多处可能出现的事务安全问题 2023-12-14 13:07:57 +08:00
xboard b8009142ed perf: 优化数据库备份Command逻辑 2023-12-13 09:28:01 +08:00
xboard f812e5f239 fix: 修复创建工单中的事务安全 2023-12-13 03:00:43 +08:00
xboard e16618142a feat: 增加Loon自动下发Hysteria2节点 2023-12-13 00:08:39 +08:00
xboard 145d3580a0 fix:[用户前端] 修复markdown-body类中暗黑模式不生效的问题 2023-12-12 22:18:24 +08:00
xboard 83957db37f perf: 优化节点后端获取用户用户列表接口响应速度,降低CPU使用资源 2023-12-12 21:49:16 +08:00
xboard 6d575649cf fix: 修复1.8 版本sing-box订阅失败的问题 2023-12-12 09:25:58 +08:00
xboard 4c6096224d feat: [用户前端]增加crisp识别用户email、套餐详情支持 2023-12-12 08:25:26 +08:00
xboard 1270a60ad8 refactor: 规范工单接口代码规范 2023-12-12 06:22:18 +08:00
xboard 20466d07df feat: filter支持匹配节点标签 2023-12-10 18:09:58 +08:00
xboard e04d82961f refactor: 规范部分API接口响应格式 2023-12-10 17:53:31 +08:00
xboard 7bca6e1953 fix: [用户前端]修复工单聊天窗口没有自动将聊天记录拉到最下面的问题、修复续费订阅会显示弹窗的 问题、补充部分i18n语言包 2023-12-10 17:27:28 +08:00
xboard 26593eda27 fix: 修复安装Command不清除缓存的问题 2023-12-10 11:51:56 +08:00
xboard d91e973639 perf:[用户前端] 补全i18n、优化登录注册交互逻辑、修复佣金提现对话框取消按钮无效的问题 2023-12-09 10:41:50 +08:00
xboard b060ab6315 feat: 个人中心页面增加绑定机器人、立即进入群聊卡片 2023-12-08 23:00:23 +08:00
xboard 16f84216f5 docs: update aapanel+docker安装指南.md 2023-12-08 20:33:10 +08:00
xboard c85ca31718 feat: 增加订阅地址支持[*-*]表达式 2023-12-08 20:16:24 +08:00
xboard 9192d5c11f docs: 修复错别字 2023-12-08 16:04:11 +08:00
xboard aa0af37e91 to: 工单增强消息调整 2023-12-08 15:51:29 +08:00
xboard ca63cbf801 feat: 工单Bot增强 2023-12-08 15:43:17 +08:00
xboard 08bafe09a7 update: composer.json 2023-12-08 10:11:37 +08:00
xboard b3b04cfc54 perf: 优化初始化安装流程 2023-12-08 08:53:19 +08:00
xboard 08c8f46eb3 update: composer.json 2023-12-07 23:12:32 +08:00
xboard b0a504a44c fix: telegram机器人setwebhook接口url设置为站点网址,防止一些反向代理配置导致绑定机器人失败的问题 2023-12-07 23:12:05 +08:00
xboard fe5e448dfb perf:[用户前端] 优化一键订阅列表 2023-12-07 21:46:11 +08:00
xboard 5b9b93ffe0 fix: [用户前端]修复ipad会被识别为mac的问题 2023-12-07 20:44:51 +08:00
xboard 0db0ea3a6b fix: 修复邀请信息获取接口返回数据格式错误的问题 2023-12-07 09:52:23 +08:00
xboard 385404dcf7 feat:[用户前端]添加订单二维码结账方式的支持、知识库增加copy和jump两个事件的支持 2023-12-07 06:09:41 +08:00
xboard fc329c6428 fix: 修复创建Hy节点时obfs默认关闭却不提交到后端的问题、优化Vless Reality节点创建字段提示 2023-12-07 04:45:39 +08:00
xboard 546f11bdae fix: 修复后台发送测试邮件返回状态不正确的问题 2023-12-07 04:19:30 +08:00
xboard 189b247ad8 refactor: 规范状态码、抛出异常的使用 2023-12-07 04:01:32 +08:00
xboard c25803aa74 Log: 给流量消费队列当中保存失败的用户增加错误日志 2023-12-06 19:11:29 +08:00
xboard 1fcb6fa911 fix: 规范数据库事物的使用,解决在swoole环境下可能会出现事物一直不被提交的问题 2023-12-06 19:00:26 +08:00
xboard 64cc2d79da fix: 优化流量消费队列,防止记录长时间被锁住 2023-12-04 23:16:18 +08:00
xboard 66ab4a4a8e fix: 修复潮汐app Trojan协议的问题 2023-12-04 22:52:11 +08:00
xboard e4a80ce9b5 fix:[用户前端] 修复订阅详情为数字时的异常渲染 2023-12-04 20:42:34 +08:00
xboard 0ab7dee52d refactor: 规范Expection处理 2023-12-04 20:40:49 +08:00
xboard aa0fe64afe 在 Docker 环境中读取 INSTALL 环境变量判断是否已经安装 2023-12-04 20:30:38 +08:00
xboard 4a6339c9db fix: 修复hiddify无法自动下发hy2节点的问题 2023-12-03 22:17:33 +08:00
xboard 8d4846329e fix: 修复SingBox下发带混淆的hysteria失败的问题 2023-12-03 19:21:05 +08:00
xboard 2a768b6fc8 feat: [用户前端]订阅介绍支持潮汐客户端的写法 2023-12-03 18:37:32 +08:00
xboard bf3930d29f fix: 修复tg机器人无法回复工单的问题 2023-12-03 09:43:38 +08:00
xboard ea6cd5eca1 style:[用户前端] Markdown使用github样式预设 2023-12-03 07:16:53 +08:00
xboard e1f6cd70df fix:[用户前端] 修复订阅介绍为空时订阅不显示的问题,修复公告中图片宽度会超出边界的问题 2023-12-03 00:19:19 +08:00
xboard 4ec52b7033 feat:[用户前端] 提高复制功能兼容性、订阅描述增加md支持、增加使用文档预设样式 2023-12-02 22:04:48 +08:00
xboard 1d66023bd1 refactor: CORS中间件改用\Illuminate\Http\Middleware\HandleCors 2023-12-01 00:36:09 +08:00
xboard e35f81f2c8 debug:[用户前端]删除测试代码 2023-12-01 00:23:58 +08:00
xboard 6a825dca7d style:[用户前端] 将一键订阅的软件图标内联到网页当中,加快图标的加载速度 2023-12-01 00:18:39 +08:00
xboard 849b887201 fix:[用户前端] 修复前端显示三级分销比率错误的问题 2023-11-29 04:50:53 +08:00
xboard 7a2f4c1d56 fix:[用户前端] 修复个人中心流量邮件提醒关闭无效的问题 2023-11-28 23:17:04 +08:00
xboard 4aa19ebae9 fix:[用户前端] 修复手机端菜单栏被数据表格覆盖的问题 2023-11-28 21:57:27 +08:00
xboard f31b194a53 i18n:[用户前端] 更新语言包 2023-11-28 21:39:16 +08:00
xboard 177eefc447 feat:[用户前端] 增加提现推广用户按钮,修正三级分销当中佣金比例显示问题 2023-11-28 21:31:32 +08:00
xboard f1f1ed4040 fix:[用户前端] 修复长期套餐用户不显示重置已用流量按钮的问题,补全 重置按钮语言包 2023-11-28 20:29:56 +08:00
xboard dcb57c6c37 style:[用户前端] 增加首页增加重置流量按钮、知识库增加img、video标签最大宽度的为100%的限制、首页增加流量告急警告、修改手机端菜单栏关闭样式 2023-11-28 19:28:20 +08:00
xboard aa59624208 fix: [用户前端]修复google reCaptcha 超时导致错误的问题、补全部分语言包 2023-11-27 16:19:48 +08:00
xboard 4eadd201e5 feat:[用户前端] 增加多语言支持,修复菜单栏存在的BUG, 启用静态gzip以节省CPU资源 2023-11-27 00:23:25 +08:00
xboard 8793e82f69 docs: 增加修改代码需要重启的注意事项 2023-11-26 11:56:44 +08:00
xboard b7d28cb36f chore: 增加docker-compose 模版的自动启动的配置 2023-11-26 11:50:56 +08:00
xboard 25317d5f64 to: 添加前端静态资源请求版本号,防止更新导致的缓存 2023-11-25 10:24:14 +08:00
xboard e086456777 chore: 修改Docker环境安装过程中默认的数据库地址为127.0.0.1 2023-11-24 17:03:48 +08:00
xboard 7dc8a0ac6b style:[用户前端] 订阅列表修改为点击可片即可进入订阅详情 2023-11-24 13:40:26 +08:00
xboard 8fb63c94d3 fix:[用户前端] 修复注册页面选择邮箱后缀在手机端被缩略的问题 2023-11-24 11:53:35 +08:00
xboard 8f2662b254 chore: Docker镜像增加git软件包、修正aapanel+ docker 更新步骤 2023-11-24 11:24:15 +08:00
xboard 89745e1214 docs: update docker-compose安装指南 2023-11-24 10:50:29 +08:00
xboard 18c08b6aa3 fix:[用户前端] 修复确认中的永久与累计获得的佣金显示单位不正确的问题 2023-11-24 09:29:37 +08:00
xboard 4d30a8ade5 feat: 新增hiddify自动下发hy2 2023-11-24 00:02:33 +08:00
xboard d776ca32c5 fix: 修复用户订阅到期提醒、流量告急提醒的默认设置修改无效的问题 2023-11-23 23:59:08 +08:00
xboard 6aadeb5b84 fix: 修复迁移配置文件时配置为数组时的错误 2023-11-23 18:35:18 +08:00
xboard d832015136 feat: 增加用户流量告急提醒和订阅到期提醒的默认设置 2023-11-23 18:31:15 +08:00
xboard 1fb43f2cda fix: 修复admin_setting可能出现的问题 2023-11-23 18:02:50 +08:00
xboard 265842e7bf fix: 修复自动备份command 的问题,增加备份后自动压缩为.gz节省空间 2023-11-23 17:55:58 +08:00
xboard 0303a74a33 feat: 添加cmfa、nekoray、verge、clashx meta 自动下发Hy2 2023-11-23 15:11:22 +08:00
xboard adc56ddd82 docs: update aapanel+docker安装指南.md 2023-11-23 14:05:38 +08:00
xboard d63aec2b3c feat: 增加 新增、修改Vless、Hysteria节点表单的字段的必填提示 2023-11-23 13:37:41 +08:00
xboard 6c2cb47bf8 refactor: 优化安装命令的代码 2023-11-23 13:32:39 +08:00
xboard 653884e7b3 chore: 增加docker-compose.sample.yaml 模版 2023-11-23 13:29:30 +08:00
xboard 095e5131e8 chore: 删除docker-compose.yaml 增加docker-compose.yaml模版 2023-11-23 13:28:38 +08:00
xboard 4d0da8c4d2 update readme.md 2023-11-23 11:49:50 +08:00
xboard 7797bd2a9b docs: 补充迁移文档中缺少的步骤 2023-11-23 11:48:21 +08:00
xboard e9664e294b Merge branch 'dev' of github.com:cedar2025/v2board into dev 2023-11-23 11:13:28 +08:00
xboard 5ab84eb233 docs: 修正aapanel安装指南中的错误 2023-11-23 11:12:33 +08:00
xboard 5af382dbc5 style:[用户前端] 取消头像显示 2023-11-23 00:13:18 +08:00
xboard 8cdaf4b9ed docs: update aapanel+docker安装指南.md 2023-11-23 00:12:48 +08:00
Xboard 10fd966959 Merge pull request #7 from rebecca554owen/patch-1
修改单词拼写错误。
2023-11-22 23:25:27 +08:00
xboard 638dbef30a fix: 修复知识库 access start 在webman中重新定义的问题 2023-11-22 21:33:19 +08:00
rebecca554owen d090d03da0 Update 从aapanel迁移到1panel教程.md 2023-11-22 19:38:56 +08:00
Xboard 1b8ca0baae Merge pull request #6 from rebecca554owen/dev
内置Redis后修改教程。
2023-11-22 19:35:03 +08:00
rebecca554owen 2ae815385b Update 从aapanel迁移到1panel教程.md 2023-11-22 19:33:01 +08:00
rebecca554owen aa3a1bebc8 Update 多开Xboard教程.md 2023-11-22 19:32:26 +08:00
rebecca554owen bb7b9ee591 Merge branch 'cedar2025:dev' into dev 2023-11-22 19:31:45 +08:00
rebecca554owen 34d33aae8e Update 从aapanel迁移到1panel教程.md 2023-11-22 18:58:26 +08:00
xboard 4c1c024afa docs: 修正aapanel安装指南当中配置php.ini的命令 2023-11-22 18:52:58 +08:00
rebecca554owen 110eccd9e4 Update 从aapanel迁移到1panel教程.md 2023-11-22 18:48:01 +08:00
rebecca554owen 72ca2d4564 Update 从aapanel迁移到1panel教程.md 2023-11-22 18:42:52 +08:00
Xboard 1633764419 Update aapanel安装指南.md 2023-11-22 18:33:37 +08:00
xboard 933572648c to: 增加迁移迁移配置文件前清除缓存 2023-11-22 18:32:29 +08:00
xboard ed3e40790e docs: 更新文档,增加迁移迁移配置文件前清除缓存 2023-11-22 18:26:19 +08:00
Xboard 57b3f14de6 Update 从aapanel迁移到1panel教程.md 2023-11-22 18:16:47 +08:00
xboard db9f48ade0 docs: 补充aapanel + docker部署教程 2023-11-22 15:52:07 +08:00
xboard 65ca30a920 fix: 安装步骤当中修复错误的redis unix地址 2023-11-22 14:36:23 +08:00
Xboard 76bf2e3a53 Merge pull request #5 from rebecca554owen/dev
修正教程错误
2023-11-22 14:13:11 +08:00
xboard d1b48623d7 docs: 优化部署、迁移文档、docker增加redis支持
1、优化部署、迁移
2、自动备份命令增加手动备份功能
3、docker部署集成redis
2023-11-22 14:01:58 +08:00
rebecca554owen 2dc35a21da 多开Xboard建多个站点
多开Redis容器以便于多开Xboard容器
2023-11-22 13:37:33 +08:00
rebecca554owen 633226fddc Update 从aapanel迁移到1panel教程.md 2023-11-22 13:09:33 +08:00
rebecca554owen c1a8a283f3 Add files via upload 2023-11-22 13:05:07 +08:00
xboard 57a1d0ba48 style:[用户前端] 修复公告背景图显示不正常的问题 2023-11-22 07:50:12 +08:00
xboard 983ec8fcf3 style: 修改后台Hysteri节点颜色为蓝色 2023-11-21 19:25:30 +08:00
xboard 76cf93a2c9 chore: docker 环境增加json、ico文件访问支持 2023-11-21 19:15:32 +08:00
xboard 14c88f23ff style:[用户前端] 知识库添加Markdown支持 2023-11-21 16:59:55 +08:00
xboard 8db622eee4 perf: 优化用户流量消费队列(上万用户流量信息数秒即可处理完成 2023-11-21 15:59:06 +08:00
xboard 9d2da393d7 style:[用户前端] 修复手机浏览器登录、注册、找回密码 点显示密码按钮无反应 2023-11-21 08:09:35 +08:00
xboard 1e68b02ed9 style:[用户前端] 修复登录、注册、找回密码页面LOGO不居中的问题 2023-11-21 07:58:21 +08:00
xboard ddf5f8bd9d style:[用户前端] 修复注册、登录、找回密码界面LOGO过大溢出的问题 2023-11-21 07:53:57 +08:00
xboard aa29b37144 fix: [用户前端]修复仪表盘页公告不显示背景的问题 2023-11-20 22:38:23 +08:00
xboard 699785f995 style: [用户界面]手机端增加侧边栏关闭按钮 2023-11-20 22:04:35 +08:00
xboard f5ddd08ebf style: [用户前端]公告支持Markdown渲染的同时增加HTML渲染,优化登录、注册、找回密码页面显示效果 2023-11-20 20:54:43 +08:00
xboard f5ae16866d docs:增加laravels测试数据 2023-11-20 18:37:44 +08:00
xboard 82b6bce847 docs: 增加性能测试、页面展示 2023-11-20 18:06:42 +08:00
Xboard a8a627c918 Update readme.md 2023-11-20 14:01:47 +08:00
xboard d1b3b739d3 fix: 修复mysql5.7和sqlite环境下无法上报流量的问题 2023-11-20 13:53:11 +08:00
xboard ca1ee9fc2b docs: update readme 2023-11-20 12:40:42 +08:00
xboard 6ea3adbd55 docs: 更新迁移引导 2023-11-20 11:56:38 +08:00
xboard ae8cfa40be feat: php artisan reset:password重置密码增加可自定义密码 2023-11-20 09:38:14 +08:00
xboard bc80d7e91c refactor: 规范MysqlLogger代码 2023-11-20 09:37:22 +08:00
xboard 907ccebfdf style(clahs): 恢复默认原版clash模版 2023-11-20 09:36:21 +08:00
xboard 79b94168ba style: 公告修改为markdown格式显示 2023-11-19 22:07:10 +08:00
xboard 6b920a59f3 fix: [用户前端]修复查看教程不动态显示站点名称 2023-11-19 21:37:55 +08:00
xboard 4a66919e5c fix: 修复sqlite执行数据库字段修改迁移出错的问题 2023-11-19 21:04:52 +08:00
xboard 2dd0dfac87 fix: 修改v2_setting的value为text类型,防止过长的配置抱错 2023-11-19 20:51:52 +08:00
xboard aab33be34a style: [用户前端]优化手机端体验 2023-11-19 20:42:39 +08:00
xboard 4346720855 fix: [用户前端]修复主页订阅过期用户点击续费订阅无效的问题 2023-11-19 15:27:03 +08:00
xboard 7392165189 docs: 修改文档目录 2023-11-19 15:18:32 +08:00
xboard 6773c87d8e feat: [用户前端]补全可选订阅类型 2023-11-19 14:15:52 +08:00
xboard 4353fe1bee fix: 修复使用邀请链接注册不会自动填写邀请码的问题、修复webman环境下节点不可排序的问题 2023-11-19 12:48:37 +08:00
xboard e0eac8f703 feat: 兼容V2bx 的Hysteria2 2023-11-19 09:54:40 +08:00
xboard b489f8f6f1 fix: 修复soga后端、Tidalab、等一些后端对接获取配置失败的问题
修复添加了服务后端中间件统一鉴权导致了一些后端鉴权不通过的问题
2023-11-19 04:03:09 +08:00
xboard cc1dc14c84 feat: 添加·定时自动备份并上传到谷歌云存储·的功能 2023-11-18 19:54:16 +08:00
xboard aa9ec41921 feat: 增加Xboard主题自定义页脚HTML支持 2023-11-18 16:58:01 +08:00
xboard 519841c323 fix: 修复Vless Grpc ws不可用的问题 2023-11-18 15:31:10 +08:00
xboard cdc766c72b update: updata.sh 2023-11-18 14:44:44 +08:00
xboard 4ab3eb9ce8 fix: [用户前端]修复订单折扣金额不计算的问题 2023-11-18 12:26:34 +08:00
xboard 56485f01b2 fix: [用户前端]修复开启邮箱后缀白名单后用户注册的问题 2023-11-18 10:59:19 +08:00
xboard 2125f32e53 fix: 去除默认clash订阅中的 quic协议的dns防止clash客户端不可用 2023-11-18 09:56:33 +08:00
xboard a02881fcaf to: 添加alive空接口 2023-11-18 09:10:55 +08:00
xboard b5fae8f571 style: [用户前端]优化流量明细页面显示效果,docker环境支持wasm文件支持 2023-11-18 03:29:46 +08:00
xboard e8e5a9b955 fix: [用户前端] 流量明细记录时间精度修改为天 2023-11-17 16:37:09 +08:00
xboard b5ec7459a6 fix: 修复vless的 ws、grpc订阅问题 2023-11-17 16:34:16 +08:00
xboard aa57264d53 fix: [用户前端]修复暗黑模式下的登录页面背景颜色显示问题 2023-11-17 15:37:04 +08:00
xboard f6ac12ee65 fix: 修复meta 的vless grpc订阅下发错误 2023-11-17 15:11:52 +08:00
xboard 65fe7682ff Initial commit 2023-11-17 14:44:01 +08:00
469 changed files with 54638 additions and 58 deletions
Executable → Regular
View File
+1
View File
@@ -0,0 +1 @@
* * * * * php /www/artisan schedule:run >> /dev/null 2>&1
+44
View File
@@ -0,0 +1,44 @@
server {
listen 7001 default_server;
listen [::]:7001 default_server;
root /www/public/;
index index.html index.htm;
server_name _;
# 开启 brotli 压缩
brotli on;
brotli_static on;
brotli_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# 开启 gzip 压缩
gzip on;
gzip_static on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location ~* \.(jpg|jpeg|png|gif|js|css|svg|woff2|woff|ttf|eot|wasm|json|ico|html|htm)$ {
}
location ~ .* {
proxy_pass http://127.0.0.1:7010;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
}
location ~ /\.ht {
deny all;
}
access_log off;
error_log /dev/null crit;
}
+89
View File
@@ -0,0 +1,89 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
[unix_http_server]
file=/run/supervisord.sock
chmod=0700
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[program:chown]
directory=/www
command=sh -c "chown -R www:www /www && chmod -R 775 /www"
autostart=true
autorestart=false
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
user=root
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
[program:cron]
command=crond -f -l 8
user=root
priority=4
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
; [program:laravels]
; command=php bin/laravels start
; directory=/www
; user=www-data
; numprocs=1
; stdout_logfile=/dev/stdout
; stdout_logfile_maxbytes=0
; stderr_logfile=/dev/stderr
; stderr_logfile_maxbytes=0
; autostart=true
; autorestart=true
; startretries=3
[program:adapterman]
command=php -c php.ini webman.php start
directory=/www
user=www
numprocs=1
priority=2
startsecs=3
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
[program:xboard-queue]
command=php artisan horizon
directory=/www
user=www
priority=3
stdout_logfile=/www/storage/logs/queue.log
stdout_logfile_maxbytes=0
stderr_logfile=/www/storage/logs/queue_error.log
stderr_logfile_maxbytes=0
autostart=true
autorestart=true
startretries=10
+5
View File
@@ -0,0 +1,5 @@
FROM redis:7-alpine
RUN mkdir -p /run/redis-socket && chmod 777 /run/redis-socket
COPY ./redis.conf /etc/redis.conf
CMD ["redis-server", "/etc/redis.conf"]
+7
View File
@@ -0,0 +1,7 @@
unixsocket /run/redis-socket/redis.sock
unixsocketperm 777
port 0
save 900 1
save 300 10
save 60 10000
+25
View File
@@ -0,0 +1,25 @@
/node_modules
/config/v2board.php
/public/hot
/public/storage
/public/env.example.js
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
.idea
.lock
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
composer.phar
composer.lock
yarn.lock
docker-compose.yml
.DS_Store
/docker
storage/laravels.conf
storage/laravels.pid
storage/laravels-timer-process.pid
Executable
+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
+1 -2
View File
@@ -4,8 +4,7 @@ APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
APP_DEBUG=false
APP_URL=http://localhost
APP_RUNNING_IN_CONSOLE=true
ADMIN_SETTING_CACHE=60 #设置缓存时间(单位秒)
LOG_CHANNEL=stack
DB_CONNECTION=mysql
Executable
+5
View File
@@ -0,0 +1,5 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore
@@ -0,0 +1,43 @@
---
name: Bug report | 问题反馈
about: Tell us what problems you have encountered
title: "[BUG]"
labels: ''
assignees: ''
---
🙇‍♂️🙇‍♂️🙇‍♂️注意:XrayR等非XBoard问题请前往项目方提问
🙇‍♂️🙇‍♂️🙇‍♂️Note: XrayR and other non-XBoard issues please go to the project side to ask questions
The XBoard version number you are using
当前使用的XBoard版本号(git commit id)
--------
Would you like to deploy using Docker?
你的部署方式(是否为Docker
--------
Please briefly describe the issue you encountered (preferably with reproducible steps).
简单描述你遇到的问题(最好带上复现步骤)
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
Run the php artisan log:export 7 command to export log files (where 7 represents logs for the last 7 days).
运行`php artisan log:export 7` 命令导出的日志文件(其中7为最近7天的日志)。
--------
@@ -0,0 +1,11 @@
---
name: Feature request | 功能请求
about: Tell us what you need
title: "[Feature request]"
labels: ''
assignees: ''
---
Please describe in detail the problems or needs you have encountered.
请详细描述你遇到的问题或需求。
+94
View File
@@ -0,0 +1,94 @@
name: Docker Build and Publish
on:
push:
branches: ["legacy", "dev"]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: 'arm64,amd64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
driver-opts: |
image=moby/buildkit:latest
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,format=long
type=raw,value=new
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
- name: Update version in app.php
run: |
VERSION=$(date '+%Y%m%d')-$(git rev-parse --short HEAD)
sed -i "s/'version' => '.*'/'version' => '$VERSION'/g" config/app.php
echo "Updated version to: $VERSION"
- name: Build and push
id: build-and-push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
tags: |
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:legacy
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:dev
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }}
build-args: |
BUILDKIT_INLINE_CACHE=1
provenance: false
- name: Install cosign
uses: sigstore/cosign-installer@v3.4.0
with:
cosign-release: 'v2.2.2'
- name: Sign image
if: steps.build-and-push.outputs.digest != ''
env:
COSIGN_EXPERIMENTAL: 1
run: |
echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes "{}@${{ steps.build-and-push.outputs.digest }}"
Executable
+30
View File
@@ -0,0 +1,30 @@
/node_modules
/config/v2board.php
/config/googleCloudStorageKey.json
/public/hot
/public/storage
/public/env.example.js
*.user.ini
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
.idea
.lock
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
composer.phar
composer.lock
yarn.lock
docker-compose.yml
.DS_Store
/docker
storage/laravels.conf
storage/laravels.pid
storage/laravels-timer-process.pid
cli-php.ini
frontend
docker-compose.yaml
+18
View File
@@ -0,0 +1,18 @@
FROM phpswoole/swoole:php8.1-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pcntl bcmath inotify \
&& apk --no-cache add shadow supervisor nginx sqlite nginx-mod-http-brotli mysql-client git patch \
&& addgroup -S -g 1000 www && adduser -S -G www -u 1000 www
#复制项目文件以及配置文件
WORKDIR /www
COPY .docker /
COPY . /www
RUN composer install --optimize-autoloader --no-cache --no-dev \
&& php artisan storage:link \
&& cp /www/.env.example /www/.env \
&& chown -R www:www /www \
&& chmod -R 775 /www
CMD ["/usr/bin/supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Tokumeikoi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+64
View File
@@ -0,0 +1,64 @@
# About Xboard
Xboard is a panel based on V2board's secondary development, with significant enhancements in both performance and functionality.
# Disclaimer
This project is personally developed and maintained by me for learning purposes. I do not guarantee any availability and am not responsible for any consequences resulting from the use of this software.
# Xboard Features
Based on V2board's secondary development, with the following added features:
- Upgraded to Laravel 10
- Adapted to Laravels (10+ times concurrent improvement)
- Adapted to Webman (about 50% faster than laravels)
- Modified configuration retrieval from database
- Support for Docker deployment and distributed deployment
- Support for subscription distribution based on user IP location
- Added Hy2 support
- Added sing-box distribution
- Support for obtaining real visitor IP directly from Cloudflare
- Support for automatic new protocol distribution based on client version
- Support for route filtering (add &filter=HongKong|USA after subscription URL)
- Support for Sqlite installation (alternative to MySQL, great for personal use)
- User frontend rebuilt using Vue3 + TypeScript + NaiveUI + Unocss + Pinia
- Fixed numerous bugs
# **System Architecture**
- PHP8.1+
- Composer
- MySQL5.7+
- Redis
- Laravel
## Performance Comparison [View Details](./docs/性能对比.md)
> xboard shows tremendous performance improvements in both frontend and backend
|Scenario | php-fpm(traditional) | php-fpm(traditional with opcache) | laravels | webman(docker)|
|---- | ---- |---- |---- | ---|
|Homepage | 6 req/s | 157 req/s | 477 req/s| 803 req/s|
|User Subscription| 6 req/s | 196 req/s | 586 req/s| 1064 req/s|
|User Homepage Latency| 308ms | 110ms | 101ms | 98ms|
## Page Display
![Example Image](./docs/images/dashboard.png)
## Installation / Update / Rollback
You can click to view the **installation and update** steps for the following methods:
- [1panel Deployment](./docs/1panel安装指南.md)
- [Docker Compose Command-line Quick Deployment](./docs/docker-compose安装指南.md)
- [aapanel + Docker Compose (Recommended)](./docs/aapanel+docker安装指南.md)
- [aapanel Deployment](./docs/aapanel安装指南.md)
### Migrating from Other Versions
#### Database Migration
**Check the corresponding migration guide according to your version**
- v2board dev version 23/10/27 [Jump to Migration Guide](./docs/v2b_dev迁移指南.md)
- v2board 1.7.4 [Jump to Migration Guide](./docs/v2b_1.7.4迁移指南.md)
- v2board 1.7.3 [Jump to Migration Guide](./docs/v2b_1.7.3迁移指南.md)
- v2board wyx2685 [Jump to Migration Guide](./docs/v2b_wyx2685迁移指南.md)
### Note
> Modifying the admin path requires a restart to take effect
```
docker compose restart
```
> If using aapanel installation, you need to restart the webman daemon process
+99
View File
@@ -0,0 +1,99 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Google\Cloud\Storage\StorageClient;
use Symfony\Component\Process\Process;
class BackupDatabase extends Command
{
protected $signature = 'backup:database {upload?}';
protected $description = '备份数据库并上传到 Google Cloud Storage';
public function handle()
{
$isUpload = $this->argument('upload');
// 如果是上传到云端则判断是否存在必要配置
if($isUpload){
$requiredConfigs = ['database.connections.mysql', 'cloud_storage.google_cloud.key_file', 'cloud_storage.google_cloud.storage_bucket'];
foreach ($requiredConfigs as $config) {
if (blank(config($config))) {
$this->error("❌:缺少必要配置项: $config 取消备份");
return;
}
}
}
// 数据库备份逻辑
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_' . config('database.connections.mysql.database') . '_database_backup.sql');
$compressedBackupPath = $databaseBackupPath . '.gz';
try{
if (config('database.default') === 'mysql'){
$this->info("1️⃣:开始备份Mysql");
\Spatie\DbDumper\Databases\MySql::create()
->setHost(config('database.connections.mysql.host'))
->setPort(config('database.connections.mysql.port'))
->setDbName(config('database.connections.mysql.database'))
->setUserName(config('database.connections.mysql.username'))
->setPassword(config('database.connections.mysql.password'))
->dumpToFile($databaseBackupPath);
$this->info("2️⃣:Mysql备份完成");
}elseif(config('database.default') === 'sqlite'){
$databaseBackupPath = storage_path('backup/' . now()->format('Y-m-d_H-i-s') . '_sqlite' . '_database_backup.sql');
$this->info("1️⃣:开始备份Sqlite");
\Spatie\DbDumper\Databases\Sqlite::create()
->setDbName(config('database.connections.sqlite.database'))
->dumpToFile($databaseBackupPath);
$this->info("2️⃣:Sqlite备份完成");
}else{
$this->error('备份失败,你的数据库不是sqlite或者mysql');
return;
}
$this->info('3️⃣:开始压缩备份文件');
// 使用 gzip 压缩备份文件
$compressedBackupPath = $databaseBackupPath . '.gz';
$gzipCommand = new Process(["gzip", "-c", $databaseBackupPath]);
$gzipCommand->run();
// 检查压缩是否成功
if ($gzipCommand->isSuccessful()) {
// 压缩成功,你可以删除原始备份文件
file_put_contents($compressedBackupPath, $gzipCommand->getOutput());
$this->info('4️⃣:文件压缩成功');
unlink($databaseBackupPath);
} else {
// 压缩失败,处理错误
echo $gzipCommand->getErrorOutput();
$this->error('😔:文件压缩失败');
unlink($databaseBackupPath);
return;
}
if (!$isUpload){
$this->info("🎉:数据库成功备份到:$compressedBackupPath");
}else{
// 传到云盘
$this->info("5️⃣:开始将备份上传到Google Cloud");
// Google Cloud Storage 配置
$storage = new StorageClient([
'keyFilePath' => config('cloud_storage.google_cloud.key_file'),
]);
$bucket = $storage->bucket(config('cloud_storage.google_cloud.storage_bucket'));
$objectName = 'backup/' . now()->format('Y-m-d_H-i-s') . '_database_backup.sql.gz';
// 上传文件
$bucket->upload(fopen($compressedBackupPath, 'r'), [
'name' => $objectName,
]);
// 输出文件链接
\Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
$this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
\File::delete($compressedBackupPath);
}
}catch(\Exception $e){
\Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
$this->error("😔:数据库备份失败\n" . $e);
\File::delete($compressedBackupPath);
}
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
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
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:commission';
/**
* 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->autoCheck();
$this->autoPayCommission();
}
public function autoCheck()
{
if ((int)admin_setting('commission_auto_check_enable', 1)) {
Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->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) {
try{
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();
} catch (\Exception $e){
DB::rollBack();
throw $e;
}
}
}
public function payHandle($inviteUserId, Order $order)
{
$level = 3;
if ((int)admin_setting('commission_distribution_enable', 0)) {
$commissionShareLevels = [
0 => (int)admin_setting('commission_distribution_l1'),
1 => (int)admin_setting('commission_distribution_l2'),
2 => (int)admin_setting('commission_distribution_l3')
];
} else {
$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 (!$commissionBalance) continue;
if ((int)admin_setting('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;
// update order actual commission balance
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
}
return true;
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
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 Illuminate\Support\Facades\DB;
class CheckOrder extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:order';
/**
* 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()
{
ini_set('memory_limit', -1);
$orders = Order::whereIn('status', [Order::STATUS_PENDING, Order::STATUS_PROCESSING])
->orderBy('created_at', 'ASC')
->get();
foreach ($orders as $order) {
OrderHandleJob::dispatch($order->trade_no);
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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()
{
$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));
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use Illuminate\Console\Command;
class CheckTicket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:ticket';
/**
* 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()
{
ini_set('memory_limit', -1);
$tickets = Ticket::where('status', 0)
->where('updated_at', '<=', time() - 24 * 3600)
->where('reply_status', 0)
->get();
foreach ($tickets as $ticket) {
if ($ticket->user_id === $ticket->last_reply_user_id) continue;
$ticket->status = Ticket::STATUS_CLOSED;
$ticket->save();
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Console\Command;
class ClearUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'clear:user';
/**
* 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()
{
$builder = User::where('plan_id', NULL)
->where('transfer_enable', 0)
->where('expired_at', 0)
->where('last_login_at', NULL);
$count = $builder->count();
if ($builder->delete()) {
$this->info("已删除${count}位没有任何数据的用户");
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Carbon\Carbon;
class ExportV2Log extends Command
{
protected $signature = 'log:export {days=1 : The number of days to export logs for}';
protected $description = 'Export v2_log table records of the specified number of days to a file';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$days = $this->argument('days');
$date = Carbon::now()->subDays($days)->startOfDay();
$logs = \DB::table('v2_log')
->where('created_at', '>=', $date->timestamp)
->get();
$fileName = "v2_logs_" . Carbon::now()->format('Y_m_d_His') . ".csv";
$handle = fopen(storage_path("logs/$fileName"), 'w');
// 根据您的表结构
fputcsv($handle, ['Level', 'ID', 'Title', 'Host', 'URI', 'Method', 'Data', 'IP', 'Context', 'Created At', 'Updated At']);
foreach ($logs as $log) {
fputcsv($handle, [
$log->level,
$log->id,
$log->title,
$log->host,
$log->uri,
$log->method,
$log->data,
$log->ip,
$log->context,
Carbon::createFromTimestamp($log->created_at)->toDateTimeString(),
Carbon::createFromTimestamp($log->updated_at)->toDateTimeString()
]);
}
fclose($handle);
$this->info("日志成功导出到: ". storage_path("logs/$fileName"));
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
class MigrateFromV2b extends Command
{
protected $signature = 'migrateFromV2b {version?}';
protected $description = '供不同版本V2b迁移到本项目的脚本';
public function handle()
{
$version = $this->argument('version');
if($version === 'config'){
$this->MigrateV2ConfigToV2Settings();
return;
}
// Define your SQL commands based on versions
$sqlCommands = [
'dev231027' => [
// SQL commands for version Dev 2023/10/27
'ALTER TABLE v2_order ADD COLUMN surplus_order_ids TEXT NULL;',
'ALTER TABLE v2_plan DROP COLUMN daily_unit_price, DROP COLUMN transfer_unit_price;',
'ALTER TABLE v2_server_hysteria DROP COLUMN ignore_client_bandwidth, DROP COLUMN obfs_type;'
],
'1.7.4' => [
'CREATE TABLE `v2_server_vless` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`group_id` TEXT NOT NULL,
`route_id` TEXT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` INT NULL,
`host` VARCHAR(255) NOT NULL,
`port` INT NOT NULL,
`server_port` INT NOT NULL,
`tls` BOOLEAN NOT NULL,
`tls_settings` TEXT NULL,
`flow` VARCHAR(64) NULL,
`network` VARCHAR(11) NOT NULL,
`network_settings` TEXT NULL,
`tags` TEXT NULL,
`rate` VARCHAR(11) NOT NULL,
`show` BOOLEAN DEFAULT 0,
`sort` INT NULL,
`created_at` INT NOT NULL,
`updated_at` INT NOT NULL
);'
],
'1.7.3' => [
'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;',
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';",
"ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
"ALTER TABLE `v2_stat`
ADD COLUMN order_count INT NULL,
ADD COLUMN order_total INT NULL,
ADD COLUMN register_count INT NULL,
ADD COLUMN invite_count INT NULL,
ADD COLUMN transfer_used_total VARCHAR(32) NULL;
",
"CREATE TABLE `v2_log` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` TEXT NOT NULL,
`level` VARCHAR(11) NULL,
`host` VARCHAR(255) NULL,
`uri` VARCHAR(255) NOT NULL,
`method` VARCHAR(11) NOT NULL,
`data` TEXT NULL,
`ip` VARCHAR(128) NULL,
`context` TEXT NULL,
`created_at` INT NOT NULL,
`updated_at` INT NOT NULL
);",
'CREATE TABLE `v2_server_hysteria` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`group_id` VARCHAR(255) NOT NULL,
`route_id` VARCHAR(255) NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` INT NULL,
`host` VARCHAR(255) NOT NULL,
`port` VARCHAR(11) NOT NULL,
`server_port` INT NOT NULL,
`tags` VARCHAR(255) NULL,
`rate` VARCHAR(11) NOT NULL,
`show` BOOLEAN DEFAULT FALSE,
`sort` INT NULL,
`up_mbps` INT NOT NULL,
`down_mbps` INT NOT NULL,
`server_name` VARCHAR(64) NULL,
`insecure` BOOLEAN DEFAULT FALSE,
`created_at` INT NOT NULL,
`updated_at` INT NOT NULL
);',
"CREATE TABLE `v2_server_vless` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`group_id` TEXT NOT NULL,
`route_id` TEXT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` INT NULL,
`host` VARCHAR(255) NOT NULL,
`port` INT NOT NULL,
`server_port` INT NOT NULL,
`tls` BOOLEAN NOT NULL,
`tls_settings` TEXT NULL,
`flow` VARCHAR(64) NULL,
`network` VARCHAR(11) NOT NULL,
`network_settings` TEXT NULL,
`tags` TEXT NULL,
`rate` VARCHAR(11) NOT NULL,
`show` BOOLEAN DEFAULT FALSE,
`sort` INT NULL,
`created_at` INT NOT NULL,
`updated_at` INT NOT NULL
);",
],
'wyx2685' => [
"ALTER TABLE `v2_plan` DROP COLUMN `device_limit`;",
"ALTER TABLE `v2_server_hysteria` DROP COLUMN `version`, DROP COLUMN `obfs`, DROP COLUMN `obfs_password`;",
"ALTER TABLE `v2_server_trojan` DROP COLUMN `network`, DROP COLUMN `network_settings`;",
"ALTER TABLE `v2_user` DROP COLUMN `device_limit`;"
]
];
if (!$version) {
$version = $this->choice('请选择你迁移前的V2board版本:', array_keys($sqlCommands));
}
if (array_key_exists($version, $sqlCommands)) {
try {
foreach ($sqlCommands[$version] as $sqlCommand) {
// Execute SQL command
\DB::statement($sqlCommand);
}
$this->info('1️⃣、数据库差异矫正成功');
// 初始化数据库迁移
$this->call('db:seed', ['--class' => 'OriginV2bMigrationsTableSeeder']);
$this->info('2️⃣、数据库迁移记录初始化成功');
$this->call('xboard:update');
$this->info('3️⃣、更新成功');
$this->info("🎉:成功从 $version 迁移到Xboard");
} catch (\Exception $e) {
// An error occurred, rollback the transaction
$this->error('迁移失败'. $e->getMessage() );
}
} else {
$this->error("你所输入的版本未找到");
}
}
public function MigrateV2ConfigToV2Settings()
{
\Artisan::call('config:clear');
$configValue = config('v2board') ?? [];
foreach ($configValue as $k => $v) {
// 检查记录是否已存在
$existingSetting = Setting::where('name', $k)->first();
// 如果记录不存在,则插入
if ($existingSetting) {
$this->warn("配置 ${k} 在数据库已经存在, 忽略");
continue;
}
Setting::create([
'name' => $k,
'value' => is_array($v)? json_encode($v) : $v,
]);
$this->info("配置 ${k} 迁移成功");
}
\Artisan::call('config:cache');
$this->info('所有配置迁移完成');
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Log;
use App\Models\Plan;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetLog extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:log';
/**
* 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()
{
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetPassword extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:password {email} {password?}';
/**
* 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()
{
$password = $this->argument('password') ;
$user = User::where('email', $this->argument('email'))->first();
if (!$user) abort(500, '邮箱不存在');
$password = $password ?? Helper::guid(false);
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->password_algo = null;
if (!$user->save()) abort(500, '重置失败');
$this->info("!!!重置成功!!!");
$this->info("新密码为:{$password},请尽快修改密码。");
}
}
+201
View File
@@ -0,0 +1,201 @@
<?php
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.
*
* @var string
*/
protected $signature = 'reset:traffic';
/**
* The console command description.
*
* @var string
*/
protected $description = '流量清空';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->builder = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
ini_set('memory_limit', -1);
$resetMethods = Plan::select(
DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
DB::raw("reset_traffic_method as method")
)
->groupBy('reset_traffic_method')
->get()
->toArray();
foreach ($resetMethods as $resetMethod) {
$planIds = explode(',', $resetMethod['plan_ids']);
switch (true) {
case ($resetMethod['method'] === NULL): {
$resetTrafficMethod = admin_setting('reset_traffic_method', 0);
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
switch ((int) $resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($builder);
break;
// expire day
case 1:
$this->resetByExpireDay($builder);
break;
// no action
case 2:
break;
// year first day
case 3:
$this->resetByYearFirstDay($builder);
// year expire day
case 4:
$this->resetByExpireYear($builder);
}
break;
}
case ($resetMethod['method'] === 0): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByMonthFirstDay($builder);
break;
}
case ($resetMethod['method'] === 1): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireDay($builder);
break;
}
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone ($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
}
}
}
}
private function resetByExpireYear($builder): void
{
$users = $builder->with('plan')->get();
$usersToUpdate = [];
foreach ($users as $user) {
$expireDay = date('m-d', $user->expired_at);
$today = date('m-d');
if ($expireDay === $today) {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
private function resetByYearFirstDay($builder): void
{
$users = $builder->with('plan')->get();
$usersToUpdate = [];
foreach ($users as $user) {
if ((string) date('md') === '0101') {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
private function resetByMonthFirstDay($builder): void
{
$users = $builder->with('plan')->get();
$usersToUpdate = [];
foreach ($users as $user) {
if ((string) date('d') === '01') {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay($builder): void
{
$lastDay = date('d', strtotime('last day of +0 months'));
$today = date('d');
$users = $builder->with('plan')->get();
$usersToUpdate = [];
foreach ($users as $user) {
$expireDay = date('d', $user->expired_at);
if ($expireDay === $today || ($today === $lastDay && $expireDay >= $today)) {
$usersToUpdate[] = [
'id' => $user->id,
'transfer_enable' => $user->plan->transfer_enable
];
}
}
foreach ($usersToUpdate as $userData) {
User::where('id', $userData['id'])->update([
'transfer_enable' => (intval($userData['transfer_enable']) * 1073741824),
'u' => 0,
'd' => 0
]);
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetUser extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:user';
/**
* 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()
{
if (!$this->confirm("确定要重置所有用户安全信息吗?")) {
return;
}
ini_set('memory_limit', -1);
$users = User::all();
foreach ($users as $user)
{
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
$user->save();
$this->info("已重置用户{$user->email}的安全信息");
}
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Services\MailService;
use Illuminate\Console\Command;
use App\Models\User;
class SendRemindMail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'send:remindMail';
/**
* The console command description.
*
* @var string
*/
protected $description = '发送提醒邮件';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $mailService->remindExpire($user);
if ($user->remind_traffic) $mailService->remindTraffic($user);
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Test extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test';
/**
* 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()
{
}
}
+260
View File
@@ -0,0 +1,260 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Encryption\Encrypter;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Support\Env;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\text;
use function Laravel\Prompts\note;
class XboardInstall extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:install';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 初始化安装';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
try {
$isDocker = env('docker', false);
$enableSqlite = env('enable_sqlite', false);
$enableRedis = env('enable_redis', false);
$adminAccount = env('admin_account', '');
$this->info("__ __ ____ _ ");
$this->info("\ \ / /| __ ) ___ __ _ _ __ __| | ");
$this->info(" \ \/ / | __ \ / _ \ / _` | '__/ _` | ");
$this->info(" / /\ \ | |_) | (_) | (_| | | | (_| | ");
$this->info("/_/ \_\|____/ \___/ \__,_|_| \__,_| ");
if (
(\File::exists(base_path() . '/.env') && $this->getEnvValue('INSTALLED'))
|| (env('INSTALLED', false) && $isDocker)
) {
$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key'))));
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
$this->warn("如需重新安装请清空目录下 .env 文件的内容(Docker安装方式不可以删除此文件)");
$this->warn("快捷清空.env命令:");
note('rm .env && touch .env');
return;
}
if (is_dir(base_path() . '/.env')) {
$this->error('😔:安装失败,Docker环境下安装请保留空的 .env 文件');
return;
}
// 选择是否使用Sqlite
if ($enableSqlite || confirm(label: '是否启用Sqlite(无需额外安装)代替Mysql', default: false, yes: '启用', no: '不启用')) {
$sqliteFile = '.docker/.data/database.sqlite';
if (!file_exists(base_path($sqliteFile))) {
// 创建空文件
if (!touch(base_path($sqliteFile))) {
$this->info("sqlite创建成功: $sqliteFile");
}
}
$envConfig = [
'DB_CONNECTION' => 'sqlite',
'DB_DATABASE' => $sqliteFile,
'DB_HOST' => '',
'DB_USERNAME' => '',
'DB_PASSWORD' => '',
];
try {
\Config::set("database.default", 'sqlite');
\Config::set("database.connections.sqlite.database", base_path($envConfig['DB_DATABASE']));
\DB::purge('sqlite');
\DB::connection('sqlite')->getPdo();
if (!blank(\DB::connection('sqlite')->getPdo()->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(\PDO::FETCH_COLUMN))) {
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '退出安装')) {
$this->info('正在清空数据库请稍等');
$this->call('db:wipe', ['--force' => true]);
$this->info('数据库清空完成');
} else {
return;
}
}
} catch (\Exception $e) {
// 连接失败,输出错误消息
$this->error("数据库连接失败:" . $e->getMessage());
}
} else {
$isMysqlValid = false;
while (!$isMysqlValid) {
$envConfig = [
'DB_CONNECTION' => 'mysql',
'DB_HOST' => text(label: "请输入数据库地址", default: '127.0.0.1', required: true),
'DB_PORT' => text(label: '请输入数据库端口', default: '3306', required: true),
'DB_DATABASE' => text(label: '请输入数据库名', default: 'xboard', required: true),
'DB_USERNAME' => text(label: '请输入数据库用户名', default: 'root', required: true),
'DB_PASSWORD' => text(label: '请输入数据库密码', required: false),
];
try {
\Config::set("database.default", 'mysql');
\Config::set("database.connections.mysql.host", $envConfig['DB_HOST']);
\Config::set("database.connections.mysql.port", $envConfig['DB_PORT']);
\Config::set("database.connections.mysql.database", $envConfig['DB_DATABASE']);
\Config::set("database.connections.mysql.username", $envConfig['DB_USERNAME']);
\Config::set("database.connections.mysql.password", $envConfig['DB_PASSWORD']);
\DB::purge('mysql');
\DB::connection('mysql')->getPdo();
$isMysqlValid = true;
if (!blank(\DB::connection('mysql')->select('SHOW TABLES'))) {
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
$this->info('正在清空数据库请稍等');
$this->call('db:wipe', ['--force' => true]);
$this->info('数据库清空完成');
} else {
$isMysqlValid = false;
}
}
} catch (\Exception $e) {
// 连接失败,输出错误消息
$this->error("数据库连接失败:" . $e->getMessage());
$this->info("请重新输入数据库配置");
}
}
}
$envConfig['APP_KEY'] = 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC'));
$envConfig['INSTALLED'] = true;
$isReidsValid = false;
while (!$isReidsValid) {
// 判断是否为Docker环境
if ($isDocker == 'true' && ($enableRedis || confirm(label: '是否启用Docker内置的Redis', default: true, yes: '启用', no: '不启用'))) {
$envConfig['REDIS_HOST'] = '/run/redis-socket/redis.sock';
$envConfig['REDIS_PORT'] = 0;
$envConfig['REDIS_PASSWORD'] = null;
} else {
$envConfig['REDIS_HOST'] = text(label: '请输入Redis地址', default: '127.0.0.1', required: true);
$envConfig['REDIS_PORT'] = text(label: '请输入Redis端口', default: '6379', required: true);
$envConfig['REDIS_PASSWORD'] = text(label: '请输入redis密码(默认: null)', default: '');
}
$redisConfig = [
'client' => 'phpredis',
'default' => [
'host' => $envConfig['REDIS_HOST'],
'password' => $envConfig['REDIS_PASSWORD'],
'port' => $envConfig['REDIS_PORT'],
'database' => 0,
],
];
try {
$redis = new \Illuminate\Redis\RedisManager(app(), 'phpredis', $redisConfig);
$redis->ping();
$isReidsValid = true;
} catch (\Exception $e) {
// 连接失败,输出错误消息
$this->error("redis连接失败:" . $e->getMessage());
$this->info("请重新输入REDIS配置");
}
}
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
;
$email = !empty($adminAccount) ? $adminAccount : text(
label: '请输入管理员账号',
default: 'admin@demo.com',
required: true,
validate: fn(string $email): ?string => match (true) {
!filter_var($email, FILTER_VALIDATE_EMAIL) => '请输入有效的邮箱地址.',
default => null,
}
);
$password = Helper::guid(false);
$this->saveToEnv($envConfig);
$this->call('config:cache');
\Artisan::call('cache:clear');
$this->info('正在导入数据库请稍等...');
\Artisan::call("migrate", ['--force' => true]);
$this->info(\Artisan::output());
$this->info('数据库导入完成');
$this->info('开始注册管理员账号');
if (!$this->registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试');
}
$this->info('🎉:一切就绪');
$this->info("管理员邮箱:{$email}");
$this->info("管理员密码:{$password}");
$defaultSecurePath = hash('crc32b', config('app.key'));
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
} catch (\Exception $e) {
$this->error($e);
}
}
public function registerAdmin($email, $password)
{
$user = new User();
$user->email = $email;
if (strlen($password) < 8) {
abort(500, '管理员密码长度最小为8位字符');
}
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
$user->is_admin = 1;
return $user->save();
}
private function set_env_var($key, $value)
{
$value = !strpos($value, ' ') ? $value : '"' . $value . '"';
$key = strtoupper($key);
$envPath = app()->environmentFilePath();
$contents = file_get_contents($envPath);
if (preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches)) {
$contents = str_replace($matches[0], "{$key}={$value}", $contents);
} else {
$contents .= "\n{$key}={$value}\n";
}
return file_put_contents($envPath, $contents) !== false;
}
private function saveToEnv($data = [])
{
foreach ($data as $key => $value) {
self::set_env_var($key, $value);
}
return true;
}
function getEnvValue($key, $default = null)
{
$dotenv = \Dotenv\Dotenv::createImmutable(base_path());
$dotenv->load();
return Env::get($key, $default);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class XboardRollback extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:rollback';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 回滚';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('正在回滚数据库请稍等...');
\Artisan::call("migrate:rollback");
$this->info(\Artisan::output());
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Console\Commands;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Services\StatisticalService;
use Illuminate\Console\Command;
use App\Models\Stat;
use Illuminate\Support\Facades\DB;
class XboardStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard: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()
{
$startAt = microtime(true);
ini_set('memory_limit', -1);
$this->statUser();
$this->statServer();
$this->stat();
info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
}
private function statServer()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatServer();
foreach ($stats as $stat) {
if (!StatServer::insert([
'server_id' => $stat['server_id'],
'server_type' => $stat['server_type'],
'u' => $stat['u'],
'd' => $stat['d'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat server fail');
}
}
DB::commit();
$statService->clearStatServer();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function statUser()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatUser();
foreach ($stats as $stat) {
if (!StatUser::insert([
'user_id' => $stat['user_id'],
'u' => $stat['u'],
'd' => $stat['d'],
'server_rate' => $stat['server_rate'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat user fail');
}
}
DB::commit();
$statService->clearStatUser();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function stat()
{
try {
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($startAt);
$statisticalService->setEndAt($endAt);
$data = $statisticalService->generateStatData();
$data['record_at'] = $startAt;
$data['record_type'] = 'd';
$statistic = Stat::where('record_at', $startAt)
->where('record_type', 'd')
->first();
if ($statistic) {
$statistic->update($data);
return;
}
Stat::create($data);
} catch (\Exception $e) {
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class XboardUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'xboard:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'xboard 更新';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('正在导入数据库请稍等...');
\Artisan::call("migrate");
$this->info(\Artisan::output());
\Artisan::call('horizon:terminate');
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Console;
use App\Utils\CacheKey;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Cache;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
Cache::put(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null), time());
// v2board
$schedule->command('xboard:statistics')->dailyAt('0:10')->onOneServer();
// check
$schedule->command('check:order')->everyMinute()->onOneServer();
$schedule->command('check:commission')->everyMinute()->onOneServer();
$schedule->command('check:ticket')->everyMinute()->onOneServer();
// reset
$schedule->command('reset:traffic')->daily()->onOneServer();
$schedule->command('reset:log')->daily()->onOneServer();
// send
$schedule->command('send:remindMail')->dailyAt('11:30')->onOneServer();
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
// backup Timing
if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
$schedule->command('backup:database', ['true'])->daily()->onOneServer();
}
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__ . '/Commands');
require base_path('routes/console.php');
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Exceptions;
use Exception;
class ApiException extends Exception
{
protected $code; // 错误码
protected $message; // 错误消息
protected $errors; // 全部错误信息
public function __construct($message = null, $code = 400, $errors = null)
{
$this->message = $message;
$this->code = $code;
$this->errors = $errors;
}
public function errors(){
return $this->errors;
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Exceptions;
use Exception;
class BusinessException extends Exception
{
/**
* 业务异常构造函数
* @param array $codeResponse 状态码
* @param string $info 自定义返回信息,不为空时会替换掉codeResponse 里面的message文字信息
*/
public function __construct(array $codeResponse, $info = '')
{
[$code, $message] = $codeResponse;
parent::__construct($info ?: $message, $code);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Exceptions;
use App\Helpers\ApiResponse;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr;
use Illuminate\View\ViewException;
use Throwable;
class Handler extends ExceptionHandler
{
use ApiResponse;
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
ApiException::class
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($exception instanceof ViewException) {
return $this->fail([500, '主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。']);
}
// ApiException主动抛出错误
if ($exception instanceof ApiException) {
$code = $exception->getCode();
$message = $exception->getMessage();
$errors = $exception->errors();
return $this->fail([$code, $message],null,$errors);
}
return parent::render($request, $exception);
}
protected function convertExceptionToArray(Throwable $e)
{
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(function ($trace) {
return Arr::except($trace, ['args']);
})->all(),
] : [
'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."),
];
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
namespace App\Helpers;
use App\Helpers\ResponseEnum;
use App\Exceptions\BusinessException;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
trait ApiResponse
{
/**
* 成功
* @param mixed $data
* @param array $codeResponse
* @return JsonResponse
*/
public function success($data = null, $codeResponse=ResponseEnum::HTTP_OK): JsonResponse
{
return $this->jsonResponse('success', $codeResponse, $data, null);
}
/**
* 失败
* @param array $codeResponse
* @param mixed $data
* @param mixed $error
* @return JsonResponse
*/
public function fail($codeResponse=ResponseEnum::HTTP_ERROR, $data = null, $error=null): JsonResponse
{
return $this->jsonResponse('fail', $codeResponse, $data, $error);
}
/**
* json响应
* @param $status
* @param $codeResponse
* @param $data
* @param $error
* @return JsonResponse
*/
private function jsonResponse($status, $codeResponse, $data, $error): JsonResponse
{
list($code, $message) = $codeResponse;
return response()
->json([
'status' => $status,
// 'code' => $code,
'message' => $message,
'data' => $data ?? null,
'error' => $error,
],(int)substr(((string) $code),0,3));
}
/**
* 成功分页返回
* @param $page
* @return JsonResponse
*/
protected function successPaginate($page): JsonResponse
{
return $this->success($this->paginate($page));
}
private function paginate($page)
{
if ($page instanceof LengthAwarePaginator){
return [
'total' => $page->total(),
'page' => $page->currentPage(),
'limit' => $page->perPage(),
'pages' => $page->lastPage(),
'list' => $page->items()
];
}
if ($page instanceof Collection){
$page = $page->toArray();
}
if (!is_array($page) && !is_object($page)){
return $page;
}
$total = count($page);
return [
'total' => $total, //数据总数
'page' => 1, // 当前页码
'limit' => $total, // 每页的数据条数
'pages' => 1, // 最后一页的页码
'list' => $page // 数据
];
}
/**
* 业务异常返回
* @param array $codeResponse
* @param string $info
* @throws BusinessException
*/
public function throwBusinessException(array $codeResponse=ResponseEnum::HTTP_ERROR, string $info = '')
{
throw new BusinessException($codeResponse, $info);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
use App\Support\Setting;
if (! function_exists("get_request_content")){
function get_request_content(){
return request()->getContent() ?: json_encode($_POST);
}
}
if (! function_exists('admin_setting')) {
/**
* 获取或保存配置参数.
*
* @param string|array $key
* @param mixed $default
* @return App\Support\Setting|mixed
*/
function admin_setting($key = null, $default = null)
{
if ($key === null) {
return App::make(Setting::class)->toArray();
}
if (is_array($key)) {
App::make(Setting::class)->save($key);
return '';
}
$default = config('v2board.'. $key) ?? $default;
return App::make(Setting::class)->get($key) ?? $default ;
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Helpers;
class ResponseEnum
{
// 001 ~ 099 表示系统状态;100 ~ 199 表示授权业务;200 ~ 299 表示用户业务
/*-------------------------------------------------------------------------------------------*/
// 100开头的表示 信息提示,这类状态表示临时的响应
// 100 - 继续
// 101 - 切换协议
/*-------------------------------------------------------------------------------------------*/
// 200表示服务器成功地接受了客户端请求
const HTTP_OK = [200001, '操作成功'];
const HTTP_ERROR = [200002, '操作失败'];
const HTTP_ACTION_COUNT_ERROR = [200302, '操作频繁'];
const USER_SERVICE_LOGIN_SUCCESS = [200200, '登录成功'];
const USER_SERVICE_LOGIN_ERROR = [200201, '登录失败'];
const USER_SERVICE_LOGOUT_SUCCESS = [200202, '退出登录成功'];
const USER_SERVICE_LOGOUT_ERROR = [200203, '退出登录失败'];
const USER_SERVICE_REGISTER_SUCCESS = [200104, '注册成功'];
const USER_SERVICE_REGISTER_ERROR = [200105, '注册失败'];
const USER_ACCOUNT_REGISTERED = [23001, '账号已注册'];
/*-------------------------------------------------------------------------------------------*/
// 300开头的表示服务器重定向,指向的别的地方,客户端浏览器必须采取更多操作来实现请求
// 302 - 对象已移动。
// 304 - 未修改。
// 307 - 临时重定向。
/*-------------------------------------------------------------------------------------------*/
// 400开头的表示客户端错误请求错误,请求不到数据,或者找不到等等
// 400 - 错误的请求
const CLIENT_NOT_FOUND_HTTP_ERROR = [400001, '请求失败'];
const CLIENT_PARAMETER_ERROR = [400200, '参数错误'];
const CLIENT_CREATED_ERROR = [400201, '数据已存在'];
const CLIENT_DELETED_ERROR = [400202, '数据不存在'];
// 401 - 访问被拒绝
const CLIENT_HTTP_UNAUTHORIZED = [401001, '授权失败,请先登录'];
const CLIENT_HTTP_UNAUTHORIZED_EXPIRED = [401200, '账号信息已过期,请重新登录'];
const CLIENT_HTTP_UNAUTHORIZED_BLACKLISTED = [401201, '账号在其他设备登录,请重新登录'];
// 403 - 禁止访问
// 404 - 没有找到文件或目录
const CLIENT_NOT_FOUND_ERROR = [404001, '没有找到该页面'];
// 405 - 用来访问本页面的 HTTP 谓词不被允许(方法不被允许)
const CLIENT_METHOD_HTTP_TYPE_ERROR = [405001, 'HTTP请求类型错误'];
// 406 - 客户端浏览器不接受所请求页面的 MIME 类型
// 407 - 要求进行代理身份验证
// 412 - 前提条件失败
// 413 请求实体太大
// 414 - 请求 URI 太长
// 415 不支持的媒体类型
// 416 – 所请求的范围无法满足
// 417 执行失败
// 423 锁定的错误
/*-------------------------------------------------------------------------------------------*/
// 500开头的表示服务器错误,服务器因为代码,或者什么原因终止运行
// 服务端操作错误码:500 ~ 599 开头,后拼接 3 位
// 500 - 内部服务器错误
const SYSTEM_ERROR = [500001, '服务器错误'];
const SYSTEM_UNAVAILABLE = [500002, '服务器正在维护,暂不可用'];
const SYSTEM_CACHE_CONFIG_ERROR = [500003, '缓存配置错误'];
const SYSTEM_CACHE_MISSED_ERROR = [500004, '缓存未命中'];
const SYSTEM_CONFIG_ERROR = [500005, '系统配置错误'];
// 业务操作错误码(外部服务或内部服务调用)
const SERVICE_REGISTER_ERROR = [500101, '注册失败'];
const SERVICE_LOGIN_ERROR = [500102, '登录失败'];
const SERVICE_LOGIN_ACCOUNT_ERROR = [500103, '账号或密码错误'];
const SERVICE_USER_INTEGRAL_ERROR = [500200, '积分不足'];
//501 - 页眉值指定了未实现的配置
//502 - Web 服务器用作网关或代理服务器时收到了无效响应
//503 - 服务不可用。这个错误代码为 IIS 6.0 所专用
//504 - 网关超时
//505 - HTTP 版本不受支持
/*-------------------------------------------------------------------------------------------*/
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\ApiResponse;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use DispatchesJobs, ValidatesRequests, ApiResponse;
}
+199
View File
@@ -0,0 +1,199 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave;
use App\Models\Setting;
use App\Services\MailService;
use App\Services\TelegramService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ConfigController extends Controller
{
public function getEmailTemplate()
{
$path = resource_path('views/mail/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return $this->success($files);
}
public function getThemeTemplate()
{
$path = public_path('theme/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return $this->success($files);
}
public function testSendMail(Request $request)
{
$mailLog = MailService::sendEmail([
'email' => $request->user['email'],
'subject' => 'This is xboard test email',
'template_name' => 'notify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'content' => 'This is xboard test email',
'url' => admin_setting('app_url')
]
]);
return response([
'data' => true,
'log' => $mailLog
]);
}
public function setTelegramWebhook(Request $request)
{
// 判断站点网址
$app_url = admin_setting('app_url');
if(blank($app_url)) return $this->fail([422, '请先设置站点网址']);
$hookUrl = $app_url .'/api/v1/guest/telegram/webhook?' . http_build_query([
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
]);
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$telegramService->getMe();
$telegramService->setWebhook($hookUrl);
return $this->success(true);
}
public function fetch(Request $request)
{
$key = $request->input('key');
$data = [
'invite' => [
'invite_force' => (int)admin_setting('invite_force', 0),
'invite_commission' => admin_setting('invite_commission', 10),
'invite_gen_limit' => admin_setting('invite_gen_limit', 5),
'invite_never_expire' => admin_setting('invite_never_expire', 0),
'commission_first_time_enable' => admin_setting('commission_first_time_enable', 1),
'commission_auto_check_enable' => admin_setting('commission_auto_check_enable', 1),
'commission_withdraw_limit' => admin_setting('commission_withdraw_limit', 100),
'commission_withdraw_method' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close_enable' => admin_setting('withdraw_close_enable', 0),
'commission_distribution_enable' => admin_setting('commission_distribution_enable', 0),
'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
'commission_distribution_l3' => admin_setting('commission_distribution_l3')
],
'site' => [
'logo' => admin_setting('logo'),
'force_https' => (int)admin_setting('force_https', 0),
'stop_register' => (int)admin_setting('stop_register', 0),
'app_name' => admin_setting('app_name', 'XBoard'),
'app_description' => admin_setting('app_description', 'XBoard is best!'),
'app_url' => admin_setting('app_url'),
'subscribe_url' => admin_setting('subscribe_url'),
'try_out_plan_id' => (int)admin_setting('try_out_plan_id', 0),
'try_out_hour' => (int)admin_setting('try_out_hour', 1),
'tos_url' => admin_setting('tos_url'),
'currency' => admin_setting('currency', 'CNY'),
'currency_symbol' => admin_setting('currency_symbol', '¥'),
],
'subscribe' => [
'plan_change_enable' => (int)admin_setting('plan_change_enable', 1),
'reset_traffic_method' => (int)admin_setting('reset_traffic_method', 0),
'surplus_enable' => (int)admin_setting('surplus_enable', 1),
'new_order_event_id' => (int)admin_setting('new_order_event_id', 0),
'renew_order_event_id' => (int)admin_setting('renew_order_event_id', 0),
'change_order_event_id' => (int)admin_setting('change_order_event_id', 0),
'show_info_to_server_enable' => (int)admin_setting('show_info_to_server_enable', 0),
'show_protocol_to_server_enable' => (int)admin_setting('show_protocol_to_server_enable', 0),
'default_remind_expire' => (int)admin_setting('default_remind_expire',1),
'default_remind_traffic' => (int)admin_setting('default_remind_traffic',1),
],
'frontend' => [
'frontend_theme' => admin_setting('frontend_theme', 'Xboard'),
'frontend_theme_sidebar' => admin_setting('frontend_theme_sidebar', 'light'),
'frontend_theme_header' => admin_setting('frontend_theme_header', 'dark'),
'frontend_theme_color' => admin_setting('frontend_theme_color', 'default'),
'frontend_background_url' => admin_setting('frontend_background_url'),
],
'server' => [
'server_token' => admin_setting('server_token'),
'server_pull_interval' => admin_setting('server_pull_interval', 60),
'server_push_interval' => admin_setting('server_push_interval', 60),
],
'email' => [
'email_template' => admin_setting('email_template', 'default'),
'email_host' => admin_setting('email_host'),
'email_port' => admin_setting('email_port'),
'email_username' => admin_setting('email_username'),
'email_password' => admin_setting('email_password'),
'email_encryption' => admin_setting('email_encryption'),
'email_from_address' => admin_setting('email_from_address')
],
'telegram' => [
'telegram_bot_enable' => admin_setting('telegram_bot_enable', 0),
'telegram_bot_token' => admin_setting('telegram_bot_token'),
'telegram_discuss_link' => admin_setting('telegram_discuss_link')
],
'app' => [
'windows_version' => admin_setting('windows_version'),
'windows_download_url' => admin_setting('windows_download_url'),
'macos_version' => admin_setting('macos_version'),
'macos_download_url' => admin_setting('macos_download_url'),
'android_version' => admin_setting('android_version'),
'android_download_url' => admin_setting('android_download_url')
],
'safe' => [
'email_verify' => (int)admin_setting('email_verify', 0),
'safe_mode_enable' => (int)admin_setting('safe_mode_enable', 0),
'secure_path' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
'email_whitelist_enable' => (int)admin_setting('email_whitelist_enable', 0),
'email_whitelist_suffix' => admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => admin_setting('email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)admin_setting('recaptcha_enable', 0),
'recaptcha_key' => admin_setting('recaptcha_key'),
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
'register_limit_by_ip_enable' => (int)admin_setting('register_limit_by_ip_enable', 0),
'register_limit_count' => admin_setting('register_limit_count', 3),
'register_limit_expire' => admin_setting('register_limit_expire', 60),
'password_limit_enable' => (int)admin_setting('password_limit_enable', 1),
'password_limit_count' => admin_setting('password_limit_count', 5),
'password_limit_expire' => admin_setting('password_limit_expire', 60)
]
];
if ($key && isset($data[$key])) {
return $this->success([
$key => $data[$key]
]);
};
// TODO: default should be in Dict
return $this->success($data);
}
public function save(ConfigSave $request)
{
$data = $request->validated();
$config = config('v2board');
foreach (ConfigSave::RULES as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
unset($config[$k]);
continue;
}
if (array_key_exists($k, $data)) {
$value = $data[$k];
if (is_array($value)) $value = json_encode($value);
Setting::updateOrCreate(
['name' => $k],
['name' => $k, 'value' => $value]
);
}
}
// 如果是workerman环境,则触发reload
if(isset(get_defined_constants(true)['user']['Workerman'])){
posix_kill(posix_getppid(), SIGUSR1);
}
Cache::forget('admin_settings');
// \Artisan::call('horizon:terminate'); //重启队列使配置生效
return $this->success(true);
}
}
@@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponGenerate;
use App\Http\Requests\Admin\CouponSave;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Http\Request;
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' => $coupons,
'total' => $total
]);
}
public function show(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202,'优惠券不存在']);
}
$coupon->show = !$coupon->show;
if (!$coupon->save()) {
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function generate(CouponGenerate $request)
{
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$params = $request->validated();
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
return $this->fail([500,'创建失败']);
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
}
return $this->success(true);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['show'] = 1;
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
array_push($coupons, $coupon);
}
try{
DB::beginTransaction();
if (!Coupon::insert(array_map(function ($item) use ($coupon) {
// format data
if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) {
$item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
}
if (isset($item['limit_period']) && is_array($item['limit_period'])) {
$item['limit_period'] = json_encode($coupon['limit_period']);
}
return $item;
}, $coupons))) {
throw new \Exception();
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
return $this->fail([500, '生成失败']);
}
$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 = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
return $this->fail([400202,'优惠券不存在']);
}
if (!$coupon->delete()) {
return $this->fail([500,'删除失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\KnowledgeSave;
use App\Http\Requests\Admin\KnowledgeSort;
use App\Models\Knowledge;
use Illuminate\Http\Request;
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) return $this->fail([400202,'知识不存在']);
return $this->success($knowledge);
}
$data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get();
return $this->success($data);
}
public function getCategory(Request $request)
{
return $this->success(array_keys(Knowledge::get()->groupBy('category')->toArray()));
}
public function save(KnowledgeSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
return $this->fail([500,'创建失败']);
}
} else {
try {
Knowledge::find($request->input('id'))->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'创建失败']);
}
}
return $this->success(true);
}
public function show(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
throw new ApiException('知识不存在');
}
$knowledge->show = !$knowledge->show;
if (!$knowledge->save()) {
throw new ApiException('保存失败');
}
return $this->success(true);
}
public function sort(KnowledgeSort $request)
{
try {
DB::beginTransaction();
foreach ($request->input('knowledge_ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw new ApiException('保存失败');
}
return $this->success(true);
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
return $this->fail([400202,'知识不存在']);
}
if (!$knowledge->delete()) {
return $this->fail([500,'删除失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return $this->success(Notice::orderBy('id', 'DESC')->get());
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url',
'tags'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
return $this->fail([500 ,'保存失败']);
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
return $this->fail([500 ,'保存失败']);
}
}
return $this->success(true);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([500 ,'公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202 ,'公告不存在']);
}
$notice->show = $notice->show ? 0 : 1;
if (!$notice->save()) {
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422 ,'公告ID不能为空']);
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202 ,'公告不存在']);
}
if (!$notice->delete()) {
return $this->fail([500 ,'删除失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,186 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderFetch;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
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 detail(Request $request)
{
$order = Order::find($request->input('id'));
if (!$order) return $this->fail([400202 ,'订单不存在']);
$order['commission_log'] = CommissionLog::where('trade_no', $order->trade_no)->get();
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return $this->success($order);
}
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('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->whereNotIn('status', [0, 2]);
$orderModel->where('commission_balance', '>', 0);
}
$this->filter($request, $orderModel);
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->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'];
}
}
}
return response([
'data' => $res,
'total' => $total
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
if ($order->status !== 0) return $this->fail([400 ,'只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
return $this->fail([500 ,'更新失败']);
}
return $this->success(true);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
if ($order->status !== 0) return $this->fail([400 ,'只能对待支付的订单进行操作']);
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
return $this->fail([400 ,'更新失败']);
}
return $this->success(true);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'commission_status'
]);
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400202 ,'订单不存在']);
}
try {
$order->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'更新失败']);
}
return $this->success(true);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
return $this->fail([400202 ,'该用户不存在']);
}
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
return $this->fail([400 ,'该用户还有待支付的订单,无法分配']);
}
try {
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 = Order::TYPE_RESET_TRAFFIC;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = Order::TYPE_UPGRADE;
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) {
$order->type = Order::TYPE_RENEWAL;
} else {
$order->type = Order::TYPE_NEW_PURCHASE;
}
$orderService->setInvite($user);
if (!$order->save()) {
DB::rollBack();
return $this->fail([500 ,'订单创建失败']);
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
return $this->success($order->trade_no);
}
}
@@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Payment;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PaymentController extends Controller
{
public function getPaymentMethods()
{
$methods = [];
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
array_push($methods, pathinfo($file)['filename']);
}
return $this->success($methods);
}
public function fetch()
{
$payments = Payment::orderBy('sort', 'ASC')->get();
foreach ($payments as $k => $v) {
$notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
if ($v->notify_domain) {
$parseUrl = parse_url($notifyUrl);
$notifyUrl = $v->notify_domain . $parseUrl['path'];
}
$payments[$k]['notify_url'] = $notifyUrl;
}
return $this->success($payments);
}
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return $this->success($paymentService->form());
}
public function show(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
$payment->enable = !$payment->enable;
if (!$payment->save()) return $this->fail([500 ,'保存失败']);
return $this->success(true);
}
public function save(Request $request)
{
if (!admin_setting('app_url')) {
return $this->fail([400 ,'请在站点配置中配置站点地址']);
}
$params = $request->validate([
'name' => 'required',
'icon' => 'nullable',
'payment' => 'required',
'config' => 'required',
'notify_domain' => 'nullable|url',
'handling_fee_fixed' => 'nullable|integer',
'handling_fee_percent' => 'nullable|numeric|between:0,100'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空',
'notify_domain.url' => '自定义通知域名格式有误',
'handling_fee_fixed.integer' => '固定手续费格式有误',
'handling_fee_percent.between' => '百分比手续费范围须在0-100之间'
]);
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
try {
$payment->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
$params['uuid'] = Helper::randomChar(8);
if (!Payment::create($params)) {
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
return $this->success($payment->delete());
}
public function sort(Request $request)
{
$request->validate([
'ids' => 'required|array'
], [
'ids.required' => '参数有误',
'ids.array' => '参数有误'
]);
try{
DB::beginTransaction();
foreach ($request->input('ids') as $k => $v) {
if (!Payment::find($v)->update(['sort' => $k + 1])) {
throw new \Exception();
}
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = PlanService::countActiveUsers();
$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 $this->success($plans);
}
public function save(PlanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
}
DB::beginTransaction();
// update user group id and transfer
try {
if ($request->input('force_update')) {
User::where('plan_id', $plan->id)->update([
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824,
'speed_limit' => $params['speed_limit']
]);
}
$plan->update($params);
DB::commit();
return $this->success(true);
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
}
}
if (!Plan::create($params)) {
return $this->fail([500 ,'创建失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if (Order::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在订单无法删除']);
}
if (User::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在用户无法删除']);
}
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
}
}
return $this->success($plan->delete());
}
public function update(PlanUpdate $request)
{
$updateData = $request->only([
'show',
'renew'
]);
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
}
try {
$plan->update($updateData);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'保存失败']);
}
return $this->success();
}
public function sort(PlanSort $request)
{
try{
DB::beginTransaction();
foreach ($request->input('plan_ids') as $k => $v) {
if (!Plan::find($v)->update(['sort' => $k + 1])) {
throw new \Exception();
}
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\ServerGroup;
use App\Models\ServerVmess;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
class GroupController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('group_id')) {
return $this->success([ServerGroup::find($request->input('group_id'))]);
}
$serverGroups = ServerGroup::get();
$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 $this->success($serverGroups);
}
public function save(Request $request)
{
if (empty($request->input('name'))) {
return $this->fail([422,'组名不能为空']);
}
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
} else {
$serverGroup = new ServerGroup();
}
$serverGroup->name = $request->input('name');
return $this->success($serverGroup->save());
}
public function drop(Request $request)
{
if ($request->input('id')) {
$serverGroup = ServerGroup::find($request->input('id'));
if (!$serverGroup) {
return $this->fail([400202,'组不存在']);
}
}
$servers = ServerVmess::all();
foreach ($servers as $server) {
if (in_array($request->input('id'), $server->group_id)) {
return $this->fail([400,'该组已被节点所使用,无法删除']);
}
}
if (Plan::where('group_id', $request->input('id'))->first()) {
return $this->fail([400, '该组已被订阅所使用,无法删除']);
}
if (User::where('group_id', $request->input('id'))->first()) {
return $this->fail([400, '该组已被用户所使用,无法删除']);
}
return $this->success($serverGroup->delete());
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerHysteria;
use Illuminate\Http\Request;
class HysteriaController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'route_id' => 'nullable|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required|numeric',
'up_mbps' => 'required|numeric|min:1',
'down_mbps' => 'required|numeric|min:1',
'server_name' => 'nullable',
'insecure' => 'required|in:0,1',
'alpn' => 'nullable|in:0,1,2,3',
'version' => 'nullable|in:1,2',
'is_obfs' => 'nullable'
],[
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port' => '服务端口不能为空',
'rate.required' => '倍率不能为空',
'up_mbps.required' => '上行带宽不能为空',
'down_mbps.required' => '下行带宽不能为空',
]);
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
if (!ServerHysteria::create($params)) {
return $this->fail([500,'创建失败']);
}
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点ID不存在']);
}
}
return $this->success($server->delete());
}
public function update(Request $request)
{
$request->validate([
'show' => 'in:0,1'
], [
'show.in' => '显示状态格式不正确'
]);
$params = $request->only([
'show',
]);
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerHysteria::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerHysteria::create($server->toArray());
return $this->success(true);
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
return $this->success(ServerService::getAllServers());
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->only(
'shadowsocks',
'vmess',
'trojan',
'hysteria',
'vless'
) ?? [];
try{
DB::beginTransaction();
foreach ($params as $k => $v) {
$model = 'App\\Models\\Server' . ucfirst($k);
foreach($v as $id => $sort) {
$model::where('id', $id)->update(['sort' => $sort]);
}
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerRoute;
use Illuminate\Http\Request;
class RouteController extends Controller
{
public function fetch(Request $request)
{
$routes = ServerRoute::get();
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return [
'data' => $routes
];
}
public function save(Request $request)
{
$params = $request->validate([
'remarks' => 'required',
'match' => 'required|array',
'action' => 'required|in:block,dns',
'action_value' => 'nullable'
], [
'remarks.required' => '备注不能为空',
'match.required' => '匹配值不能为空',
'action.required' => '动作类型不能为空',
'action.in' => '动作类型参数有误'
]);
$params['match'] = array_filter($params['match']);
// TODO: remove on 1.8.0
$params['match'] = json_encode($params['match']);
// TODO: remove on 1.8.0
if ($request->input('id')) {
try {
$route = ServerRoute::find($request->input('id'));
$route->update($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
}
try{
ServerRoute::create($params);
return $this->success(true);
}catch(\Exception $e){
\Log::error($e);
return $this->fail([500,'创建失败']);
}
}
public function drop(Request $request)
{
$route = ServerRoute::find($request->input('id'));
if (!$route) throw new ApiException('路由不存在');
if (!$route->delete()) throw new ApiException('删除失败');
return [
'data' => true
];
}
}
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerShadowsocksSave;
use App\Http\Requests\Admin\ServerShadowsocksUpdate;
use App\Models\ServerShadowsocks;
use Illuminate\Http\Request;
class ShadowsocksController extends Controller
{
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
}
try{
ServerShadowsocks::create($params);
return $this->success(true);
}catch(\Exception $e){
\Log::error($e);
return $this->fail([500,'创建失败']);
}
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerShadowsocksUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerShadowsocks::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerShadowsocks::create($server->toArray());
return $this->success(true);
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Models\ServerTrojan;
use Illuminate\Http\Request;
class TrojanController extends Controller
{
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerTrojan::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点ID不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerTrojanUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerTrojan::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202,'服务器不存在']);
}
ServerTrojan::create($server->toArray());
return $this->success(true);
}
}
@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerVless;
use Illuminate\Http\Request;
use ParagonIE_Sodium_Compat as SodiumCompat;
use App\Utils\Helper;
class VlessController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'group_id' => 'required',
'route_id' => 'nullable|array',
'name' => 'required',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required|in:0,1,2',
'tls_settings' => 'nullable|array',
'flow' => 'nullable|in:xtls-rprx-vision',
'network' => 'required',
'network_settings' => 'nullable|array',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required',
'show' => 'nullable|in:0,1',
'sort' => 'nullable'
],[
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port' => '服务端口不能为空',
'rate.required' => '倍率不能为空',
'network.required' => '协议不能为空',
]);
if (isset($params['tls']) && (int)$params['tls'] === 2) {
$keyPair = SodiumCompat::crypto_box_keypair();
$params['tls_settings'] = $params['tls_settings'] ?? [];
if (!isset($params['tls_settings']['public_key'])) {
$params['tls_settings']['public_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_publickey($keyPair));
}
if (!isset($params['tls_settings']['private_key'])) {
$params['tls_settings']['private_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_secretkey($keyPair));
}
if (!isset($params['tls_settings']['short_id'])) {
$params['tls_settings']['short_id'] = substr(sha1($params['tls_settings']['private_key']), 0, 8);
}
if (!isset($params['tls_settings']['server_port'])) {
$params['tls_settings']['server_port'] = "443";
}
}
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerVless::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202,'节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(Request $request)
{
$params = $request->validate([
'show' => 'nullable|in:0,1',
]);
$server = ServerVless::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerVless::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
ServerVless::create($server->toArray());
return $this->success(true);
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerVmessSave;
use App\Http\Requests\Admin\ServerVmessUpdate;
use App\Models\ServerVmess;
use Illuminate\Http\Request;
class VmessController extends Controller
{
public function save(ServerVmessSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
ServerVmess::create($params);
return $this->success(true);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '节点不存在']);
}
}
return $this->success($server->delete());
}
public function update(ServerVmessUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerVmess::find($request->input('id'));
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
try {
$server->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function copy(Request $request)
{
$server = ServerVmess::find($request->input('id'));
$server->show = 0;
if (!$server) {
return $this->fail([400202, '该服务器不存在']);
}
ServerVmess::create($server->toArray());
return $this->success(true);
}
}
@@ -0,0 +1,206 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerHysteria;
use App\Models\ServerVless;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getOverride(Request $request)
{
return [
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pending_total' => Ticket::where('status', 0)
->count(),
'commission_pending_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->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'),
'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->sum('get_amount'),
'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->sum('get_amount'),
]
];
}
public function getOrder(Request $request)
{
$statistics = Stat::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
$result[] = [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['paid_total'] / 100
];
$result[] = [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['paid_count']
];
$result[] = [
'type' => '佣金金额(已发放)',
'date' => $date,
'value' => $statistic['commission_total'] / 100
];
$result[] = [
'type' => '佣金笔数(已发放)',
'date' => $date,
'value' => $statistic['commission_count']
];
}
$result = array_reverse($result);
return [
'data' => $result
];
}
// 获取当日实时流量排行
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$stats = $statService->getStatServer();
$statistics = collect($stats)->map(function ($item){
$item['total'] = $item['u'] + $item['d'];
return $item;
})->sortByDesc('total')->values()->all();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => collect($statistics)->take(15)->all()
];
}
// 获取昨日节点流量排行
public function getServerYesterdayRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::with(['parent'])->get()->toArray(),
'v2ray' => ServerVmess::with(['parent'])->get()->toArray(),
'trojan' => ServerTrojan::with(['parent'])->get()->toArray(),
'vmess' => ServerVmess::with(['parent'])->get()->toArray(),
'hysteria' => ServerHysteria::with(['parent'])->get()->toArray(),
'vless' => ServerVless::with(['parent'])->get()->toArray(),
];
$startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$endAt = strtotime(date('Y-m-d'));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', 'd')
->limit(15)
->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'];
if($server['parent']) $statistics[$k]['server_name'] .= "({$server['parent']['name']})";
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => $statistics
];
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$builder = StatUser::orderBy('record_at', 'DESC')->where('user_id', $request->input('user_id'));
$total = $builder->count();
$records = $builder->forPage($current, $pageSize)
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$todayTraffics = $statService->getStatUserByUserID($request->input('user_id'));
if (($current == 1) && count($todayTraffics) > 0) {
foreach ($todayTraffics as $todayTraffic){
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
$records->prepend($todayTraffic);
}
};
return [
'data' => $records,
'total' => $total + count($todayTraffics),
];
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\Log as LogModel;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Contracts\SupervisorRepository;
use Laravel\Horizon\Contracts\WorkloadRepository;
use Laravel\Horizon\WaitTimeCalculator;
class SystemController extends Controller
{
public function getSystemStatus()
{
$data = [
'schedule' => $this->getScheduleStatus(),
'horizon' => $this->getHorizonStatus(),
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
];
return $this->success($data);
}
public function getQueueWorkload(WorkloadRepository $workload)
{
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
}
protected function getScheduleStatus():bool
{
return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null));
}
protected function getHorizonStatus():bool
{
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
return false;
}
return collect($masters)->contains(function ($master) {
return $master->status === 'paused';
}) ? false : true;
}
public function getQueueStats()
{
$data = [
'failedJobs' => app(JobRepository::class)->countRecentlyFailed(),
'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(),
'pausedMasters' => $this->totalPausedMasters(),
'periods' => [
'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')),
'recentJobs' => config('horizon.trim.recent'),
],
'processes' => $this->totalProcessCount(),
'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(),
'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(),
'recentJobs' => app(JobRepository::class)->countRecent(),
'status' => $this->getHorizonStatus(),
'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1),
];
return $this->success($data);
}
/**
* Get the total process count across all supervisors.
*
* @return int
*/
protected function totalProcessCount()
{
$supervisors = app(SupervisorRepository::class)->all();
return collect($supervisors)->reduce(function ($carry, $supervisor) {
return $carry + collect($supervisor->processes)->sum();
}, 0);
}
/**
* Get the number of master supervisors that are currently paused.
*
* @return int
*/
protected function totalPausedMasters()
{
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
return 0;
}
return collect($masters)->filter(function ($master) {
return $master->status === 'paused';
})->count();
}
public function getSystemLog(Request $request) {
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = LogModel::orderBy('created_at', 'DESC')
->setFilterAllowKeys('level');
$total = $builder->count();
$res = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class ThemeController extends Controller
{
private $themes;
private $path;
public function __construct()
{
$this->path = $path = public_path('theme/');
$this->themes = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
}
public function getThemes()
{
$themeConfigs = [];
foreach ($this->themes as $theme) {
$themeConfigFile = $this->path . "{$theme}/config.json";
if (!File::exists($themeConfigFile)) continue;
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig;
if (admin_setting("theme_{$theme}")) continue;
$themeService = new ThemeService($theme);
$themeService->init();
}
$data = [
'themes' => $themeConfigs,
'active' => admin_setting('frontend_theme', 'Xboard')
];
return $this->success($data);
}
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes)
]);
return $this->success(admin_setting("theme_{$payload['name']}") ?? config("theme.{$payload['name']}"));
}
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes),
'config' => 'required'
]);
$payload['config'] = json_decode(base64_decode($payload['config']), true);
if (!$payload['config'] || !is_array($payload['config'])) return $this->fail([422,'参数不正确']);
$themeConfigFile = public_path("theme/{$payload['name']}/config.json");
if (!File::exists($themeConfigFile)) return $this->fail([400202,'主题不存在']);
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) return $this->fail([422,'主题配置文件有误']);
$validateFields = array_column($themeConfig['configs'], 'field_name');
$config = [];
foreach ($validateFields as $validateField) {
$config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : '';
}
File::ensureDirectoryExists(base_path() . '/config/theme/');
// $data = var_export($config, 1);
try {
admin_setting(["theme_{$payload['name']}" => $config]);
// sleep(2);
} catch (\Exception $e) {
return $this->fail([200002, '保存失败']);
}
return $this->success($config);
}
}
@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TicketService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
return $this->fail([400202,'工单不存在']);
}
$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 $this->success($ticket);
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('updated_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
if ($request->input('reply_status') !== NULL) {
$model->whereIn('reply_status', $request->input('reply_status'));
}
if ($request->input('email') !== NULL) {
$user = User::where('email', $request->input('email'))->first();
if ($user) $model->where('user_id', $user->id);
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
$request->validate([
'id' => 'required|numeric',
'message' => 'required|string'
],[
'id.required' => '工单ID不能为空',
'message.required' => '消息不能为空'
]);
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
);
return $this->success(true);
}
public function close(Request $request)
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '工单ID不能为空'
]);
try {
$ticket = Ticket::findOrFail($request->input('id'));
$ticket->status = Ticket::STATUS_CLOSED;
$ticket->save();
return $this->success(true);
} catch (ModelNotFoundException $e) {
return $this->fail([400202, '工单不存在']);
} catch (\Exception $e) {
return $this->fail([500101, '关闭失败']);
}
}
}
@@ -0,0 +1,290 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
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\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) return $this->fail([400202,'用户不存在']);
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
return $this->success($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::select(
DB::raw('*'),
DB::raw('(u+d) as total_used')
)
->orderBy($sort, $sortType);
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->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'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl( $res[$i]['token']);
}
return response([
'data' => $res,
'total' => $total
]);
}
public function getUserInfoById(Request $request)
{
$request->validate([
'id'=> 'required|numeric'
],[
'id.required' => '用户ID不能为空'
]);
$user = User::find($request->input('id'))->load('invite_user');
return $this->success($user);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
return $this->fail([400202, '用户不存在']);
}
// 检查邮箱是否被使用
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
return $this->fail([400201, '邮箱已被使用']);
}
// 处理密码
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) {
return $this->fail([400202, '订阅计划不存在']);
}
$params['group_id'] = $plan->group_id;
}
// 处理邀请用户
if ($request->input('invite_user_email') && $inviteUser = User::where('email', $request->input('invite_user_email'))->first()) {
$params['invite_user_id'] = $inviteUser->id;
} else {
$params['invite_user_id'] = null;
}
if (isset($params['banned']) && (int)$params['banned'] === 1) {
$authService = new AuthService($user);
$authService->removeAllSession();
}
try {
$user->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
}
return $this->success(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";
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 = Helper::getSubscribeUrl($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) {
return $this->fail([400202,'订阅计划不存在']);
}
}
$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()) {
return $this->fail([400201,'邮箱已存在于系统中']);
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
return $this->fail([500,'生成失败']);
}
return $this->success(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) {
return $this->fail([400202,'订阅计划不存在']);
}
}
$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);
}
try{
DB::beginTransaction();
if (!User::insert($users)) {
throw new \Exception();
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
\Log::error($e);
return $this->fail([500,'生成失败']);
}
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
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 = Helper::getSubscribeUrl($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' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
],
'send_email_mass');
}
return $this->success(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) {
\Log::error($e);
return $this->fail([500,'处理失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$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'
&& in_array($item['cipher'], [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305'
])
) {
array_push($proxy, \App\Protocols\Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'vmess') {
array_push($proxy, \App\Protocols\Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, \App\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);
}
return(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) {
$data = [
'version' => admin_setting('windows_version'),
'download_url' => admin_setting('windows_download_url')
];
} else {
$data = [
'version' => admin_setting('macos_version'),
'download_url' => admin_setting('macos_download_url')
];
}
}else{
$data = [
'windows_version' => admin_setting('windows_version'),
'windows_download_url' => admin_setting('windows_download_url'),
'macos_version' => admin_setting('macos_version'),
'macos_download_url' => admin_setting('macos_download_url'),
'android_version' => admin_setting('android_version'),
'android_download_url' => admin_setting('android_download_url')
];
}
return $this->success($data);
}
}
@@ -0,0 +1,211 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Protocols\General;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
class ClientController extends Controller
{
// 支持hy2 的客户端版本列表
const SupportedHy2ClientVersions = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
'Shadowrocket' => '1993',
'ClashMetaForAndroid' => '2.9.0',
'Nekoray' => '3.24',
'verge' => '1.3.8',
'ClashX Meta' => '1.3.5',
'Hiddify' => '0.1.0',
'loon' => '637',
'v2rayng' => '1.9.5',
'v2rayN' => '6.31',
'surge' => '2398'
];
// allowed types
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
public function subscribe(Request $request)
{
// filter types
$types = $request->input('types', 'all');
$typesArr = $types === 'all' ? self::AllowedTypes : array_values(array_intersect(explode('|', str_replace(['|', '', ','], "|", $types)), self::AllowedTypes));
// filter keyword
$filterArr = mb_strlen($filter = $request->input('filter')) > 20 ? null : explode("|", str_replace(['|', '', ','], "|", $filter));
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
$ip = $request->input('ip', $request->ip());
// get client version
$version = preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches) ? $matches[1] : null;
$supportHy2 = $version ? collect(self::SupportedHy2ClientVersions)
->contains(fn($minVersion, $client) => stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion)) : true;
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
// get ip location
$ip2region = new \Ip2Region();
$region = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ip2region->memorySearch($ip)['region'] ?? null) : null;
// get available servers
$servers = ServerService::getAvailableServers($user);
// filter servers
$serversFiltered = $this->serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2);
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$servers = $serversFiltered;
$this->addPrefixToServerName($servers);
if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
$classFlags = explode(',', $class->flag);
foreach ($classFlags as $classFlag) {
if (stripos($flag, $classFlag) !== false) {
return $class->handle();
}
}
}
}
$class = new General($user, $servers);
return $class->handle();
}
}
/**
* Summary of serverFilter
* @param mixed $typesArr
* @param mixed $filterArr
* @param mixed $region
* @param mixed $supportHy2
* @return array
*/
private function serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2)
{
return collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2) {
if ($server['type'] == "hysteria" && $server['version'] == 2) {
if(!in_array('hysteria2', $typesArr)){
return true;
}elseif(false == $supportHy2){
return true;
}
}
if ($filterArr) {
foreach ($filterArr as $filter) {
if (stripos($server['name'], $filter) !== false || in_array($filter, $server['tags'] ?? [])) {
return false;
}
}
return true;
}
if (strpos($region, '中国') !== false) {
$excludes = $server['excludes'] ?? [];
if (empty($excludes)) {
return false;
}
foreach ($excludes as $v) {
$excludeList = explode("|", str_replace(["", ",", " ", ""], "|", $v));
foreach ($excludeList as $needle) {
if (stripos($region, $needle) !== false) {
return true;
}
}
}
}
})->values()->all();
}
/*
* add prefix to server name
*/
private function addPrefixToServerName(&$servers)
{
// 线路名称增加协议类型
if (admin_setting('show_protocol_to_server_enable')) {
$typePrefixes = [
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
$servers = collect($servers)->map(function ($server) use ($typePrefixes) {
if (isset($typePrefixes[$server['type']])) {
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
$server['name'] = $prefix . $server['name'];
}
return $server;
})->toArray();
}
}
/**
* Summary of setSubscribeInfoToServers
* @param mixed $servers
* @param mixed $user
* @param mixed $rejectServerCount
* @return void
*/
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
{
if (!isset($servers[0]))
return;
if ($rejectServerCount > 0) {
array_unshift($servers, array_merge($servers[0], [
'name' => "过滤掉{$rejectServerCount}条线路",
]));
}
if (!(int) admin_setting('show_info_to_server_enable', 0))
return;
$useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$userService = new UserService();
$resetDay = $userService->getResetDay($user);
array_unshift($servers, array_merge($servers[0], [
'name' => "套餐到期:{$expiredDate}",
]));
if ($resetDay) {
array_unshift($servers, array_merge($servers[0], [
'name' => "距离下次重置剩余:{$resetDay}",
]));
}
array_unshift($servers, array_merge($servers[0], [
'name' => "剩余流量:{$remainingTraffic}",
]));
}
/**
* 判断版本号
*/
function versionCompare($version1, $version2)
{
if (!preg_match('/^\d+(\.\d+){0,2}/', $version1) || !preg_match('/^\d+(\.\d+){0,2}/', $version2)) {
return false;
}
$v1Parts = explode('.', $version1);
$v2Parts = explode('.', $version2);
$maxParts = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxParts; $i++) {
$part1 = isset($v1Parts[$i]) ? (int) $v1Parts[$i] : 0;
$part2 = isset($v2Parts[$i]) ? (int) $v2Parts[$i] : 0;
if ($part1 < $part2) {
return false;
} elseif ($part1 > $part2) {
return true;
}
}
// 版本号相等
return true;
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Utils\Dict;
use Illuminate\Support\Facades\Http;
class CommController extends Controller
{
public function config()
{
$data = [
'tos_url' => admin_setting('tos_url'),
'is_email_verify' => (int)admin_setting('email_verify', 0) ? 1 : 0,
'is_invite_force' => (int)admin_setting('invite_force', 0) ? 1 : 0,
'email_whitelist_suffix' => (int)admin_setting('email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0,
'is_recaptcha' => (int)admin_setting('recaptcha_enable', 0) ? 1 : 0,
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
'app_description' => admin_setting('app_description'),
'app_url' => admin_setting('app_url'),
'logo' => admin_setting('logo'),
];
return $this->success($data);
}
private function getEmailSuffix()
{
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\Payment;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
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)
return $this->fail([422, 'verify error']);
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
return $this->fail([400, 'handle error']);
}
return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, 'fail']);
}
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if (!$order) {
return $this->fail([400202, 'order is not found']);
}
if ($order->status !== Order::STATUS_PENDING)
return true;
$orderService = new OrderService($order);
if (!$orderService->paid($callbackNo)) {
return false;
}
$payment = Payment::where('id', $order->payment_id)->first();
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n" .
"———————————————\n" .
"支付接口:%s\n" .
"支付渠道:%s\n" .
"本站订单:`%s`"
,
$order->total_amount / 100,
$payment->payment,
$payment->name,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use Illuminate\Http\Request;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return $this->success($plan);
}
}
@@ -0,0 +1,124 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
protected $msg;
protected $commands = [];
protected $telegramService;
public function __construct(TelegramService $telegramService)
{
$this->telegramService = $telegramService;
}
public function webhook(Request $request)
{
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) {
throw new ApiException('access_token is error', 401);
}
$data = json_decode(get_request_content(),true);
$this->formatMessage($data);
$this->formatChatJoinRequest($data);
$this->handle();
}
private function handle()
{
if (!$this->msg) return;
$msg = $this->msg;
$commandName = explode('@', $msg->command);
// To reduce request, only commands contains @ will get the bot name
if (count($commandName) == 2) {
$botName = $this->getBotName();
if ($commandName[1] === $botName){
$msg->command = $commandName[0];
}
}
try {
foreach (glob(base_path('app//Plugins//Telegram//Commands') . '/*.php') as $file) {
$command = basename($file, '.php');
$class = '\\App\\Plugins\\Telegram\\Commands\\' . $command;
if (!class_exists($class)) continue;
$instance = new $class();
if ($msg->message_type === 'message') {
if (!isset($instance->command)) continue;
if ($msg->command !== $instance->command) continue;
$instance->handle($msg);
return;
}
if ($msg->message_type === 'reply_message') {
if (!isset($instance->regex)) continue;
if (!preg_match($instance->regex, $msg->reply_text, $match)) continue;
$instance->handle($msg, $match);
return;
}
}
} catch (\Exception $e) {
$this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
}
}
private function getBotName()
{
$response = $this->telegramService->getMe();
return $response->result->username;
}
private function formatMessage(array $data)
{
if (!isset($data['message'])) return;
if (!isset($data['message']['text'])) return;
$obj = new \StdClass();
$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 = 'message';
$obj->text = $data['message']['text'];
$obj->is_private = $data['message']['chat']['type'] === 'private';
if (isset($data['message']['reply_to_message']['text'])) {
$obj->message_type = 'reply_message';
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
$this->msg = $obj;
}
private function formatChatJoinRequest(array $data)
{
if (!isset($data['chat_join_request'])) return;
if (!isset($data['chat_join_request']['from']['id'])) return;
if (!isset($data['chat_join_request']['chat']['id'])) return;
$user = \App\Models\User::where('telegram_id', $data['chat_join_request']['from']['id'])
->first();
if (!$user) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
if (!$userService->isAvailable($user)) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
$this->telegramService->approveChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
}
}
@@ -0,0 +1,302 @@
<?php
namespace App\Http\Controllers\V1\Passport;
use App\Helpers\ResponseEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthForget;
use App\Http\Requests\Passport\AuthLogin;
use App\Http\Requests\Passport\AuthRegister;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\CacheKey;
use App\Utils\Dict;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller
{
public function loginWithMailLink(Request $request)
{
if (!(int)admin_setting('login_with_mail_link_enable')) {
return $this->fail([404,null]);
}
$params = $request->validate([
'email' => 'required|email:strict',
'redirect' => 'nullable'
]);
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) {
return $this->fail([429 ,__('Sending frequently, please try again later')]);
}
$user = User::where('email', $params['email'])->first();
if (!$user) {
return $this->success(true);
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 300);
Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']), time(), 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$link = admin_setting('app_url') . $redirect;
} else {
$link = url($redirect);
}
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => __('Login to :name', [
'name' => admin_setting('app_name', 'XBoard')
]),
'template_name' => 'login',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'link' => $link,
'url' => admin_setting('app_url')
]
]);
return $this->success($link);
}
public function register(AuthRegister $request)
{
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)admin_setting('register_limit_count', 3)) {
return $this->fail([429,__('Register frequently, please try again after :minute minute', [
'minute' => admin_setting('register_limit_expire', 60)
])]);
}
}
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
return $this->fail([400,__('Invalid code is incorrect')]);
}
}
if ((int)admin_setting('email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$request->input('email'),
admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
return $this->fail([400,__('Email suffix is not in the Whitelist')]);
}
}
if ((int)admin_setting('email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
return $this->fail([400,__('Gmail alias is not supported')]);
}
}
if ((int)admin_setting('stop_register', 0)) {
return $this->fail([400,__('Registration has closed')]);
}
if ((int)admin_setting('invite_force', 0)) {
if (empty($request->input('invite_code'))) {
return $this->fail([422,__('You must use the invitation code to register')]);
}
}
if ((int)admin_setting('email_verify', 0)) {
if (empty($request->input('email_code'))) {
return $this->fail([422,__('Email verification code cannot be empty')]);
}
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
return $this->fail([400,__('Incorrect email verification code')]);
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
return $this->fail([400201,__('Email already exists')]);
}
$user = new User();
$user->email = $email;
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
// TODO 增加过期默认值、流量告急提醒默认值
$user->remind_expire = admin_setting('default_remind_expire',1);
$user->remind_traffic = admin_setting('default_remind_traffic',1);
if ($request->input('invite_code')) {
$inviteCode = InviteCode::where('code', $request->input('invite_code'))
->where('status', 0)
->first();
if (!$inviteCode) {
if ((int)admin_setting('invite_force', 0)) {
return $this->fail([400,__('Invalid invitation code')]);
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
if (!(int)admin_setting('invite_never_expire', 0)) {
$inviteCode->status = 1;
$inviteCode->save();
}
}
}
// try out
if ((int)admin_setting('try_out_plan_id', 0)) {
$plan = Plan::find(admin_setting('try_out_plan_id'));
if ($plan) {
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (admin_setting('try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
}
if (!$user->save()) {
return $this->fail([500,__('Register failed')]);
}
if ((int)admin_setting('email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$user->last_login_at = time();
$user->save();
if ((int)admin_setting('register_limit_by_ip_enable', 0)) {
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)admin_setting('register_limit_expire', 60) * 60
);
}
$authService = new AuthService($user);
$data = $authService->generateAuthData($request);
return $this->success($data);
}
public function login(AuthLogin $request)
{
$email = $request->input('email');
$password = $request->input('password');
if ((int)admin_setting('password_limit_enable', 1)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= (int)admin_setting('password_limit_count', 5)) {
return $this->fail([429,__('There are too many password errors, please try again after :minute minutes.', [
'minute' => admin_setting('password_limit_expire', 60)
])]);
}
}
$user = User::where('email', $email)->first();
if (!$user) {
return $this->fail([400, __('Incorrect email or password')]);
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
if ((int)admin_setting('password_limit_enable')) {
Cache::put(
CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
(int)$passwordErrorCount + 1,
60 * (int)admin_setting('password_limit_expire', 60)
);
}
return $this->fail([400, __('Incorrect email or password')]);
}
if ($user->banned) {
return $this->fail([400, __('Your account has been suspended')]);
}
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
}
public function token2Login(Request $request)
{
if ($request->input('token')) {
$redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (admin_setting('app_url')) {
$location = admin_setting('app_url') . $redirect;
} else {
$location = url($redirect);
}
return redirect()->to($location)->send();
}
if ($request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key);
if (!$userId) {
return $this->fail([400,__('Token error')]);
}
$user = User::find($userId);
if (!$user) {
return $this->fail([400,__('The user does not ')]);
}
if ($user->banned) {
return $this->fail([400,__('Your account has been suspended')]);
}
Cache::forget($key);
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
}
}
public function getQuickLoginUrl(Request $request)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) return $this->fail(ResponseEnum::CLIENT_HTTP_UNAUTHORIZED);
$user = AuthService::decryptAuthData($authorization);
if (!$user) return $this->fail(ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED);
$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 (admin_setting('app_url')) {
$url = admin_setting('app_url') . $redirect;
} else {
$url = url($redirect);
}
return $this->success($url);
}
public function forget(AuthForget $request)
{
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email'));
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey);
if ($forgetRequestLimit >= 3) return $this->fail([429, __('Reset failed, Please try again later')]);
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
return $this->fail([400,__('Incorrect email verification code')]);
}
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
return $this->fail([400,__('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()) {
return $this->fail([500,__('Reset failed')]);
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return $this->success(true);
}
}
@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\V1\Passport;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\CommSendEmailVerify;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\CacheKey;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
private function isEmailVerify()
{
return $this->success((int)admin_setting('email_verify', 0) ? 1 : 0);
}
public function sendEmailVerify(CommSendEmailVerify $request)
{
if ((int)admin_setting('recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(admin_setting('recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
return $this->fail([400, __('Invalid code is incorrect')]);
}
}
$email = $request->input('email');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
return $this->fail([400, __('Email verification code has been sent, please request again later')]);
}
$code = rand(100000, 999999);
$subject = admin_setting('app_name', 'XBoard') . __('Email verification code');
SendEmailJob::dispatch([
'email' => $email,
'subject' => $subject,
'template_name' => 'verify',
'template_value' => [
'name' => admin_setting('app_name', 'XBoard'),
'code' => $code,
'url' => admin_setting('app_url')
]
]);
Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $email), $code, 300);
Cache::put(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email), time(), 60);
return $this->success(true);
}
public function pv(Request $request)
{
$inviteCode = InviteCode::where('code', $request->input('invite_code'))->first();
if ($inviteCode) {
$inviteCode->pv = $inviteCode->pv + 1;
$inviteCode->save();
}
return $this->success(true);
}
private function getEmailSuffix()
{
$suffix = admin_setting('email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}
@@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerVmess;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
class DeepbworkController extends Controller
{
CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerVmess::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600);
$users = ServerService::getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => 0,
"level" => 0,
];
unset($user->uuid);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
return response(null,304);
}
return response([
'msg' => 'ok',
'data' => $result,
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
$server = ServerVmess::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_VMESS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_VMESS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
$formatData = [];
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
$userService->trafficFetch($server->toArray(), 'vmess', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$request->validate([
'node_id' => 'required',
'local_port' => 'required'
],[
'node_id.required' => '节点ID不能为空',
'local_port.required' => '本地端口不能为空'
]);
try {
$json = $this->getV2RayConfig($request->input('node_id'), $request->input('local_port'));
} catch (\Exception $e) {
\Log::error($e);
throw new ApiException($e->getMessage());
}
return(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerVmess::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)admin_setting('server_log_enable') ? 'debug' : 'none';
$json->inbounds[1]->port = (int)$localPort;
$json->inbounds[0]->port = (int)$server->server_port;
$json->inbounds[0]->streamSettings->network = $server->network;
$this->setDns($server, $json);
$this->setNetwork($server, $json);
$this->setRule($server, $json);
$this->setTls($server, $json);
return $json;
}
private function setDns(ServerVmess $server, object $json)
{
if ($server->dnsSettings) {
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
}
$json->dns = $dns;
$json->outbounds[0]->settings->domainStrategy = 'UseIP';
}
}
private function setNetwork(ServerVmess $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(ServerVmess $server, object $json)
{
$domainRules = array_filter(explode(PHP_EOL, admin_setting('server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, admin_setting('server_v2ray_protocol')));
if ($server->ruleSettings) {
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->domain)) {
$ruleSettings->domain = array_filter($ruleSettings->domain);
if (!empty($ruleSettings->domain)) {
$domainRules = array_merge($domainRules, $ruleSettings->domain);
}
}
// protocol
if (isset($ruleSettings->protocol)) {
$ruleSettings->protocol = array_filter($ruleSettings->protocol);
if (!empty($ruleSettings->protocol)) {
$protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
}
}
}
if (!empty($domainRules)) {
$domainObj = new \StdClass();
$domainObj->type = 'field';
$domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj);
}
if (!empty($protocolRules)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj);
}
if (empty($domainRules) && empty($protocolRules)) {
$json->inbounds[0]->sniffing->enabled = false;
}
}
private function setTls(ServerVMess $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = $server->tlsSettings;
$json->inbounds[0]->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
'keyFile' => '/root/.cert/server.key'
];
$json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
if (isset($tlsSettings->serverName)) {
$json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
}
if (isset($tlsSettings->allowInsecure)) {
$json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
}
$json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
}
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerShadowsocks;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Shadowsocks
* Github: https://github.com/tokumeikoi/tidalab-ss
*/
class ShadowsocksTidalabController extends Controller
{
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerShadowsocks::find($nodeId);
if (!$server) {
return $this->fail([400,'节点不存在']);
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$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
]);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
return response(null,304);
}
return response([
'data' => $result
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
$server = ServerShadowsocks::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$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();
$formatData = [];
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
$userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Trojan
* Github: https://github.com/tokumeikoi/tidalab-trojan
*/
class TrojanTidalabController extends Controller
{
const TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
return $this->fail([400, '节点不存在']);
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$users = ServerService::getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->trojan_user = [
"password" => $user->uuid,
];
unset($user->uuid);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null, 304);
}
return response([
'msg' => 'ok',
'data' => $result,
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
$server = ServerTrojan::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = get_request_content();
$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();
$formatData = [];
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
$userService->trafficFetch($server->toArray(), 'trojan', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$request->validate([
'node_id' => 'required',
'local_port' => 'required'
], [
'node_id.required' => '节点ID不能为空',
'local_port.required' => '本地端口不能为空'
]);
try {
$json = $this->getTrojanConfig($request->input('node_id'), $request->input('local_port'));
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '配置获取失败']);
}
return (json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
return $this->fail([400, '节点不存在']);
}
$json = json_decode(self::TROJAN_CONFIG);
$json->local_port = $server->server_port;
$json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
$json->ssl->cert = "/root/.cert/server.crt";
$json->ssl->key = "/root/.cert/server.key";
$json->api->api_port = $localPort;
return $json;
}
}
@@ -0,0 +1,164 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class UniProxyController extends Controller
{
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($request->input('node_type')) . '_LAST_CHECK_AT', $request->input('node_id')), time(), 3600);
$users = ServerService::getAvailableUsers($request->input('node_info')->group_id)->toArray();
$response['users'] = $users;
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null, 304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function push(Request $request)
{
$res = json_decode(get_request_content(), true);
$data = array_filter($res, function ($item) {
return is_array($item) && count($item) === 2 && is_numeric($item[0]) && is_numeric($item[1]);
});
$nodeType = $request->input('node_type');
$nodeId = $request->input('node_id');
// 增加单节点多服务器统计在线人数
$ip = $request->ip();
$id = $request->input("id");
$time = time();
$cacheKey = CacheKey::get('MULTI_SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId);
// 1、获取节点节点在线人数缓存
$onlineUsers = Cache::get($cacheKey) ?? [];
$onlineCollection = collect($onlineUsers);
// 过滤掉超过600秒的记录
$onlineCollection = $onlineCollection->reject(function ($item) use ($time) {
return $item['time'] < ($time - 600);
});
// 定义数据
$updatedItem = [
'id' => $id ?? $ip,
'ip' => $ip,
'online_user' => count($data),
'time' => $time
];
$existingItemIndex = $onlineCollection->search(function ($item) use ($updatedItem) {
return ($item['id'] ?? '') === $updatedItem['id'];
});
if ($existingItemIndex !== false) {
$onlineCollection[$existingItemIndex] = $updatedItem;
} else {
$onlineCollection->push($updatedItem);
}
$onlineUsers = $onlineCollection->all();
Cache::put($cacheKey, $onlineUsers, 3600);
$online_user = $onlineCollection->sum('online_user');
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId), $online_user, 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId), time(), 3600);
$userService = new UserService();
$userService->trafficFetch($request->input('node_info')->toArray(), $nodeType, $data, $ip);
return $this->success(true);
}
// 后端获取配置
public function config(Request $request)
{
$nodeType = $request->input('node_type');
$nodeInfo = $request->input('node_info');
switch ($nodeType) {
case 'shadowsocks':
$response = [
'server_port' => $nodeInfo->server_port,
'cipher' => $nodeInfo->cipher,
'obfs' => $nodeInfo->obfs,
'obfs_settings' => $nodeInfo->obfs_settings
];
if ($nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
$response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 16);
}
if ($nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
$response['server_key'] = Helper::getServerKey($nodeInfo->created_at, 32);
}
break;
case 'vmess':
$response = [
'server_port' => $nodeInfo->server_port,
'network' => $nodeInfo->network,
'networkSettings' => $nodeInfo->networkSettings,
'tls' => $nodeInfo->tls
];
break;
case 'trojan':
$response = [
'host' => $nodeInfo->host,
'server_port' => $nodeInfo->server_port,
'server_name' => $nodeInfo->server_name,
'network' => $nodeInfo->network,
'networkSettings' => $nodeInfo->networkSettings,
];
break;
case 'hysteria':
$response = [
'version' => $nodeInfo->version,
'host' => $nodeInfo->host,
'server_port' => $nodeInfo->server_port,
'server_name' => $nodeInfo->server_name,
'up_mbps' => $nodeInfo->up_mbps,
'down_mbps' => $nodeInfo->down_mbps,
'obfs' => $nodeInfo->is_obfs ? Helper::getServerKey($nodeInfo->created_at, 16) : null
];
break;
case "vless":
$response = [
'server_port' => $nodeInfo->server_port,
'network' => $nodeInfo->network,
'network_settings' => $nodeInfo->network_settings,
'networkSettings' => $nodeInfo->network_settings,
'tls' => $nodeInfo->tls,
'flow' => $nodeInfo->flow,
'tls_settings' => $nodeInfo->tls_settings
];
break;
}
$response['base_config'] = [
'push_interval' => (int) admin_setting('server_push_interval', 60),
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
];
if ($nodeInfo['route_id']) {
$response['routes'] = ServerService::getRoutes($nodeInfo['route_id']);
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
return response(null, 304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
// 后端提交在线数据
public function alive(Request $request)
{
return $this->success(true);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
$data = Notice::orderBy('id', 'DESC')->get();
return $this->success($data);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
return $this->fail([500, '创建失败']);
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
return $this->success(true);
}
public function drop(Request $request)
{
$request->validate([
'id' => 'required'
],[
'id.required' => '公告ID不能为空'
]);
$notice = Notice::find($request->input('id'));
if (!$notice) {
return $this->fail([400202,'公告不存在']);
}
if (!$notice->delete()) {
return $this->fail([500,'公告删除失败']);
}
return $this->success(true);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
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 $this->success($plans);
}
}
@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Services\TicketService;
use Illuminate\Http\Request;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
return $this->fail([400,'工单不存在']);
}
$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 $this->success($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();
return response([
'data' => $res,
'total' => $total
]);
}
public function reply(Request $request)
{
$request->validate([
'id' => 'required',
'message' => 'required|string'
],[
'id.required' => '工单ID不能为空',
'message.required' => '消息不能为空'
]);
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
);
return $this->success(true);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422,'工单ID不能为空']);
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
return $this->fail([400202,'工单不存在']);
}
$ticket->status = Ticket::STATUS_CLOSED;
if (!$ticket->save()) {
return $this->fail([500, '工单关闭失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\V1\Staff;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Staff\UserUpdate;
use App\Jobs\SendEmailJob;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function getUserInfoById(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422,'用户ID不能为空']);
}
$user = User::where('is_admin', 0)
->where('id', $request->input('id'))
->where('is_staff', 0)
->first();
if (!$user) return $this->fail([400202,'用户不存在']);
return $this->success($user);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
return $this->fail([400202,'用户不存在']);
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
return $this->fail([400201,'邮箱已被使用']);
}
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) {
return $this->fail([400202,'订阅不存在']);
}
$params['group_id'] = $plan->group_id;
}
try {
$user->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'更新失败']);
}
return $this->success(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' => admin_setting('app_name', 'XBoard'),
'url' => admin_setting('app_url'),
'content' => $request->input('content')
]
]);
}
return $this->success(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) {
\Log::error($e);
return $this->fail([500,'处理上失败']);
}
return $this->success(true);
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Payment;
use App\Utils\Dict;
use Illuminate\Http\Request;
class CommController extends Controller
{
public function config()
{
$data = [
'is_telegram' => (int)admin_setting('telegram_bot_enable', 0),
'telegram_discuss_link' => admin_setting('telegram_discuss_link'),
'stripe_pk' => admin_setting('stripe_pk_live'),
'withdraw_methods' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close' => (int)admin_setting('withdraw_close_enable', 0),
'currency' => admin_setting('currency', 'CNY'),
'currency_symbol' => admin_setting('currency_symbol', '¥'),
'commission_distribution_enable' => (int)admin_setting('commission_distribution_enable', 0),
'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
'commission_distribution_l3' => admin_setting('commission_distribution_l3')
];
return $this->success($data);
}
public function getStripePublicKey(Request $request)
{
$payment = Payment::where('id', $request->input('id'))
->where('payment', 'StripeCredit')
->first();
if (!$payment) throw new ApiException('payment is not found');
return $this->success($payment->config['stripe_pk_live']);
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
class CouponController extends Controller
{
public function check(Request $request)
{
if (empty($request->input('code'))) {
return $this->fail([422,__('Coupon cannot be empty')]);
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->user['id']);
$couponService->check();
return $this->success($couponService->getCoupon());
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Resources\ComissionLogResource;
use App\Http\Resources\InviteCodeResource;
use App\Models\CommissionLog;
use App\Models\InviteCode;
use App\Models\Order;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Http\Request;
class InviteController extends Controller
{
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->user['id'])->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
return $this->fail([400,__('The maximum number of creations has been reached')]);
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->user['id'];
$inviteCode->code = Helper::randomChar(8);
return $this->success($inviteCode->save());
}
public function details(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = CommissionLog::where('invite_user_id', $request->user['id'])
->where('get_amount', '>', 0)
->orderBy('created_at', 'DESC');
$total = $builder->count();
$details = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => ComissionLogResource::collection($details),
'total' => $total
]);
}
public function fetch(Request $request)
{
$commission_rate = admin_setting('invite_commission', 10);
$user = User::find($request->user['id'])
->load(['codes' => fn($query) => $query->where('status', 0)]);
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;
}
$uncheck_commission_balance = (int)Order::where('status', 3)
->where('commission_status', 0)
->where('invite_user_id', $user->id)
->sum('commission_balance');
if (admin_setting('commission_distribution_enable', 0)) {
$uncheck_commission_balance = $uncheck_commission_balance * (admin_setting('commission_distribution_l1') / 100);
}
$stat = [
//已注册用户数
(int)User::where('invite_user_id', $user->id)->count(),
//有效的佣金
(int)CommissionLog::where('invite_user_id', $user->id)
->sum('get_amount'),
//确认中的佣金
$uncheck_commission_balance,
//佣金比例
(int)$commission_rate,
//可用佣金
(int)$user->commission_balance
];
$data = [
'codes' => InviteCodeResource::collection($user->codes),
'stat' => $stat
];
return $this->success($data);
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Knowledge;
use App\Models\User;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
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) return $this->fail([500, __('Article does not exist')]);
$user = User::find($request->user['id']);
$userService = new UserService();
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$knowledge['body'] = str_replace('{{siteName}}', admin_setting('app_name', 'XBoard'), $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 $this->success($knowledge);
}
$builder = Knowledge::select(['id', 'category', 'title', 'updated_at'])
->where('language', $request->input('language'))
->where('show', 1)
->orderBy('sort', 'ASC');
$keyword = $request->input('keyword');
if ($keyword) {
$builder = $builder->where(function ($query) use ($keyword) {
$query->where('title', 'LIKE', "%{$keyword}%")
->orWhere('body', 'LIKE', "%{$keyword}%");
});
}
$knowledges = $builder->get()
->groupBy('category');
return $this->success($knowledges);
}
private function formatAccessData(&$body)
{
$pattern = '/<!--access start-->(.*?)<!--access end-->/s';
$replacement = '<div class="v2board-no-access">' . __('You must have a valid subscription to view content in this area') . '</div>';
$body = preg_replace($pattern, $replacement, $body);
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC')
->where('show', 1);
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}
+249
View File
@@ -0,0 +1,249 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Plan;
use App\Models\User;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return $this->success($order->makeHidden(['id', 'user_id']));
}
public function detail(Request $request)
{
$order = Order::where('user_id', $request->user['id'])
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist or has been paid')]);
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)admin_setting('try_out_plan_id');
if (!$order['plan']) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return $this->success($order);
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->user['id'])) {
return $this->fail([400, __('You have an unpaid or pending order, please try again later or cancel it')]);
}
$planService = new PlanService($request->input('plan_id'));
$plan = $planService->plan;
$user = User::find($request->user['id']);
if (!$plan) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
if ($user->plan_id !== $plan->id && !$planService->haveCapacity() && $request->input('period') !== 'reset_price') {
throw new ApiException(__('Current product is sold out'));
}
if ($plan[$request->input('period')] === NULL) {
return $this->fail([400, __('This payment period cannot be purchased, please choose another period')]);
}
if ($request->input('period') === 'reset_price') {
if (!$userService->isAvailable($user) || $plan->id !== $user->plan_id) {
return $this->fail([400, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package')]);
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('period') !== 'reset_price') {
return $this->fail([400, __('This subscription has been sold out, please choose another subscription')]);
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
return $this->fail([400, __('This subscription cannot be renewed, please change to another subscription')]);
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
return $this->fail([400, __('This subscription has expired, please change to another subscription')]);
}
try{
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $request->user['id'];
$order->plan_id = $plan->id;
$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)) {
return $this->fail([400, __('Coupon failed')]);
}
$order->coupon_id = $couponService->getId();
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
return $this->fail([400, __('Insufficient balance')]);
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
return $this->fail([400, __('Insufficient balance')]);
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollBack();
return $this->fail([400, __('Failed to create order')]);
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
throw $e;
}
return $this->success($order->trade_no);
}
public function checkout(Request $request)
{
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('status', 0)
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist or has been paid')]);
}
// free process
if ($order->total_amount <= 0) {
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) return $this->fail([400, '支付失败']);
return response([
'type' => -1,
'data' => true
]);
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) return $this->fail([400, __('Payment method is not available')]);
$paymentService = new PaymentService($payment->payment, $payment->id);
$order->handling_amount = NULL;
if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
$order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
}
$order->payment_id = $method;
if (!$order->save()) return $this->fail([400, __('Request failed, please try again later')]);
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
'user_id' => $order->user_id,
'stripe_token' => $request->input('token')
]);
return response([
'type' => $result['type'],
'data' => $result['data']
]);
}
public function check(Request $request)
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);
}
return $this->success($order->status);
}
public function getPaymentMethod()
{
$methods = Payment::select([
'id',
'name',
'payment',
'icon',
'handling_fee_fixed',
'handling_fee_percent'
])
->where('enable', 1)
->orderBy('sort', 'ASC')
->get();
return $this->success($methods);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
return $this->fail([422, __('Invalid parameter')]);
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->user['id'])
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);
}
if ($order->status !== 0) {
return $this->fail([400, __('You can only cancel pending orders')]);
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
return $this->fail([400, __('Cancel failed')]);
}
return $this->success(true);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))->first();
if (!$plan) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
return $this->success($plan);
}
$counts = PlanService::countActiveUsers();
$plans = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
foreach ($plans as $k => $v) {
if ($plans[$k]->capacity_limit === NULL) continue;
if (!isset($counts[$plans[$k]->id])) continue;
$plans[$k]->capacity_limit = $plans[$k]->capacity_limit - $counts[$plans[$k]->id]->count;
}
return $this->success($plans);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Resources\NodeResource;
use App\Models\User;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$servers = ServerService::getAvailableServers($user);
}
$eTag = sha1(json_encode(array_column($servers, 'cache_key')));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
return response(null,304);
}
$data = NodeResource::collection($servers);
return response([
'data' => $data
])->header('ETag', "\"{$eTag}\"");
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Resources\TrafficLogResource;
use App\Models\StatUser;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getTrafficLog(Request $request)
{
$startDate = now()->startOfMonth()->timestamp;
$records = StatUser::query()
->where('user_id', $request->user['id'])
->where('record_at', '>=', $startDate)
->orderBy('record_at', 'DESC')
->get();
// 追加当天流量
$recordAt = strtotime(date('Y-m-d'));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$todayTraffics = $statService->getStatUserByUserID($request->user['id']);
if (count($todayTraffics) > 0) {
$todayTraffics = collect($todayTraffics)->map(function ($todayTraffic) {
$todayTraffic['server_rate'] = number_format($todayTraffic['server_rate'], 2);
return $todayTraffic;
});
$records = $todayTraffics->merge($records);
}
$data = TrafficLogResource::collection(collect($records));
return $this->success($data);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\V1\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();
$data = [
'username' => $response->result->username
];
return $this->success($data);
}
public function unbind(Request $request)
{
$user = User::where('user_id', $request->user['id'])->first();
}
}
@@ -0,0 +1,224 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\TicketSave;
use App\Http\Requests\User\TicketWithdraw;
use App\Http\Resources\TicketResource;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TelegramService;
use App\Services\TicketService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first()
->load('message');
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
$ticket['message']->each(function ($message) use ($ticket) {
$message['is_me'] = ($message['user_id'] == $ticket->user_id);
});
return $this->success(TicketResource::make($ticket)->additional(['message' => true]));
}
$ticket = Ticket::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC')
->get();
return $this->success(TicketResource::collection($ticket));
}
public function save(TicketSave $request)
{
try{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->user['id'])->lockForUpdate()->count()) {
throw new \Exception(__('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
]), [
'user_id' => $request->user['id']
]));
if (!$ticket) {
throw new \Exception(__('There are other unresolved tickets'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
if (!$ticketMessage) {
throw new \Exception(__('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
return $this->success(true);
}catch(\Exception $e){
DB::rollBack();
\Log::error($e);
return $this->fail([400, $e->getMessage()]);
}
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([400, __('Invalid parameter')]);
}
if (empty($request->input('message'))) {
return $this->fail([400, __('Message cannot be empty')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
}
if ($ticket->status) {
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
}
if ($request->user['id'] == $this->getLastMessage($ticket->id)->user_id) {
return $this->fail([400, __('Please wait for the technical enginneer to reply')]);
}
$ticketService = new TicketService();
if (!$ticketService->reply(
$ticket,
$request->input('message'),
$request->user['id']
)) {
return $this->fail([400, __('Ticket reply failed')]);
}
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
return $this->success(true);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
return $this->fail([422, __('Invalid parameter')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
}
$ticket->status = Ticket::STATUS_CLOSED;
if (!$ticket->save()) {
return $this->fail([500, __('Close failed')]);
}
return $this->success(true);
}
private function getLastMessage($ticketId)
{
return TicketMessage::where('ticket_id', $ticketId)
->orderBy('id', 'DESC')
->first();
}
public function withdraw(TicketWithdraw $request)
{
if ((int)admin_setting('withdraw_close_enable', 0)) {
return $this->fail([400, 'Unsupported withdraw']);
}
if (!in_array(
$request->input('withdraw_method'),
admin_setting('commission_withdraw_method',Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
)) {
return $this->fail([422, __('Unsupported withdrawal method')]);
}
$user = User::find($request->user['id']);
$limit = admin_setting('commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]);
}
try{
DB::beginTransaction();
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->user['id']
]);
if (!$ticket) {
return $this->fail([400, __('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->user['id'],
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollBack();
return $this->fail([400, __('Failed to open ticket')]);
}
DB::commit();
}catch(\Exception $e){
DB::rollBack();
throw $e;
}
$this->sendNotify($ticket, $message, $request->user['id']);
return $this->success(true);
}
private function sendNotify(Ticket $ticket, string $message, $user_id)
{
$user = User::find($user_id)->load('plan');
$transfer_enable = $this->getFlowData($user->transfer_enable); // 总流量
$remaining_traffic = $this->getFlowData($user->transfer_enable - $user->u - $user->d); // 剩余流量
$u = $this->getFlowData($user->u); // 上传
$d = $this->getFlowData($user->d); // 下载
$expired_at = date("Y-m-d h:m:s", $user->expired_at); // 到期时间
$money = $user->balance / 100;
$affmoney = $user->commission_balance / 100;
$plan = $user->plan;
$ip = request()->ip();
$region = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? (new \Ip2Region())->simple($ip) : "NULL";
$TGmessage = "📮工单提醒 #{$ticket->id}\n———————————————\n";
$TGmessage .= "邮箱: `{$user->email}`\n";
$TGmessage .= "用户位置: \n`{$region}`\n";
if($user->plan){
$TGmessage .= "套餐与流量: \n`{$plan->name} {$transfer_enable}/{$remaining_traffic}`\n";
$TGmessage .= "上传/下载: \n`{$u}/{$d}`\n";
$TGmessage .= "到期时间: \n`{$expired_at}`\n";
}else{
$TGmessage .= "套餐与流量: \n`未订购任何套餐`\n";
}
$TGmessage .= "余额/佣金余额: \n`{$money}/{$affmoney}`\n";
$TGmessage .= "主题:\n`{$ticket->subject}`\n内容:\n`{$message}`\n";
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin($TGmessage, true);
}
private function getFlowData($b)
{
$m = $b / (1024 * 1024);
if ($m >= 1024) {
$g = $m / 1024;
$text = round($g, 2) . "GB";
} else {
$text = round($m, 2) . "MB";
}
return $text;
}
}
+218
View File
@@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserChangePassword;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\Ticket;
use App\Models\User;
use App\Services\AuthService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
$authService = new AuthService($user);
return $this->success($authService->getSessions());
}
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
$authService = new AuthService($user);
return $this->success($authService->removeSession($request->input('session_id')));
}
public function checkLogin(Request $request)
{
$data = [
'is_login' => $request->user['id'] ? true : false
];
if ($request->user['is_admin']) {
$data['is_admin'] = true;
}
return $this->success($data);
}
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$request->input('old_password'),
$user->password)
) {
return $this->fail([400, __('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()) {
return $this->fail([400, __('Save failed')]);
}
return $this->success(true);
}
public function info(Request $request)
{
$user = User::where('id', $request->user['id'])
->select([
'email',
'transfer_enable',
'last_login_at',
'created_at',
'banned',
'remind_expire',
'remind_traffic',
'expired_at',
'balance',
'commission_balance',
'plan_id',
'discount',
'commission_rate',
'telegram_id',
'uuid'
])
->first();
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return $this->success($user);
}
public function getStat(Request $request)
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->user['id'])
->count(),
Ticket::where('status', 0)
->where('user_id', $request->user['id'])
->count(),
User::where('invite_user_id', $request->user['id'])
->count()
];
return $this->success($stat);
}
public function getSubscribe(Request $request)
{
$user = User::where('id', $request->user['id'])
->select([
'plan_id',
'token',
'expired_at',
'u',
'd',
'transfer_enable',
'email',
'uuid'
])
->first();
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
}
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
$userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user);
return $this->success($user);
}
public function resetSecurity(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
return $this->fail([400, __('Reset failed')]);
}
return $this->success(Helper::getSubscribeUrl($user->token));
}
public function update(UserUpdate $request)
{
$updateData = $request->only([
'remind_expire',
'remind_traffic'
]);
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
try {
$user->update($updateData);
} catch (\Exception $e) {
return $this->fail([400, __('Save failed')]);
}
return $this->success(true);
}
public function transfer(UserTransfer $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
if ($request->input('transfer_amount') > $user->commission_balance) {
return $this->fail([400, __('Insufficient commission balance')]);
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
return $this->fail([400, __('Transfer failed')]);
}
return $this->success(true);
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
return $this->fail([400, __('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 (admin_setting('app_url')) {
$url = admin_setting('app_url') . $redirect;
} else {
$url = url($redirect);
}
return $this->success($url);
}
}
@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function override(Request $request)
{
$params = $request->validate([
'start_at' => '',
'end_at' => ''
]);
if (isset($params['start_at']) && isset($params['end_at'])) {
$stats = Stat::where('record_at', '>=', $params['start_at'])
->where('record_at', '<', $params['end_at'])
->get()
->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
->toArray();
} else {
$statisticalService = new StatisticalService();
return [
'data' => $statisticalService->generateStatData()
];
}
$stats = array_reduce($stats, function($carry, $item) {
foreach($item as $key => $value) {
if(isset($carry[$key]) && $carry[$key]) {
$carry[$key] += $value;
} else {
$carry[$key] = $value;
}
}
return $carry;
}, []);
return [
'data' => $stats
];
}
public function record(Request $request)
{
$request->validate([
'type' => 'required|in:paid_total,commission_total,register_count',
'start_at' => '',
'end_at' => ''
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getStatRecord($request->input('type'))
];
}
public function ranking(Request $request)
{
$request->validate([
'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank',
'start_at' => '',
'end_at' => '',
'limit' => 'nullable|integer'
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20)
];
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\Language::class,
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $middlewareAliases = [
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'user' => \App\Http\Middleware\User::class,
'admin' => \App\Http\Middleware\Admin::class,
'client' => \App\Http\Middleware\Client::class,
'staff' => \App\Http\Middleware\Staff::class,
'log' => \App\Http\Middleware\RequestLog::class,
'server' => \App\Http\Middleware\Server::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Services\AuthService;
use Closure;
use Illuminate\Support\Facades\Cache;
class Admin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) throw new ApiException('未登录或登陆已过期', 403);
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_admin']) throw new ApiException('未登录或登陆已过期',403);
$request->merge([
'user' => $user
]);
return $next($request);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Utils\CacheKey;
use Closure;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class Client
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$token = $request->input('token');
if (empty($token)) {
throw new ApiException('token is null',403);
}
$user = User::where('token', $token)->first();
if (!$user) {
throw new ApiException('token is error',403);
}
$request->merge([
'user' => $user
]);
return $next($request);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array
*/
protected $except = [
//
];
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ForceJson
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
$request->headers->set('accept', 'application/json');
return $next($request);
}
}
+17
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);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/home');
}
return $next($request);
}
}

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