feat: new xboard

This commit is contained in:
xboard
2025-01-21 14:57:54 +08:00
parent de18cfe596
commit 0f43fff242
373 changed files with 17923 additions and 20264 deletions

View File

@@ -1 +0,0 @@
* * * * * php /www/artisan schedule:run >> /dev/null 2>&1

View File

@@ -1,41 +0,0 @@
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;
}
}

View File

@@ -1,83 +0,0 @@
[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
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
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
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
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
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

View File

@@ -1,5 +0,0 @@
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"]

View File

@@ -1,7 +0,0 @@
unixsocket /run/redis-socket/redis.sock
unixsocketperm 777
port 0
save 900 1
save 300 10
save 60 10000

View File

@@ -0,0 +1,64 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfil=/www/storage/logs/supervisor/supervisord.pid
loglevel=info
[program:octane]
process_name=%(program_name)s_%(process_num)02d
command=php /www/artisan octane:start --host=0.0.0.0 --port=7001
autostart=%(ENV_ENABLE_WEB)s
autorestart=true
user=www
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stdout_logfile_backups=0
numprocs=1
stopwaitsecs=10
stopsignal=QUIT
stopasgroup=true
killasgroup=true
priority=100
[program:horizon]
process_name=%(program_name)s_%(process_num)02d
command=php /www/artisan horizon
autostart=%(ENV_ENABLE_HORIZON)s
autorestart=true
user=www
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stdout_logfile_backups=0
numprocs=1
stopwaitsecs=3
stopsignal=SIGINT
stopasgroup=true
killasgroup=true
priority=200
[program:redis]
process_name=%(program_name)s_%(process_num)02d
command=redis-server --dir /data
--dbfilename dump.rdb
--save 900 1
--save 300 10
--save 60 10000
--unixsocket /data/redis.sock
--unixsocketperm 777
autostart=%(ENV_ENABLE_REDIS)s
autorestart=true
user=redis
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stdout_logfile_backups=0
numprocs=1
stopwaitsecs=3
stopsignal=TERM
stopasgroup=true
killasgroup=true
priority=300

View File

@@ -23,3 +23,4 @@ docker-compose.yml
storage/laravels.conf
storage/laravels.pid
storage/laravels-timer-process.pid
/frontend

View File

@@ -4,7 +4,6 @@ APP_KEY=base64:PZXk5vTuTinfeEVG5FpYv2l6WEhLsyvGpiWK7IgJJ60=
APP_DEBUG=false
APP_URL=http://localhost
ADMIN_SETTING_CACHE=60 #设置缓存时间(单位秒)
LOG_CHANNEL=stack
DB_CONNECTION=mysql

View File

@@ -1,43 +1,39 @@
---
name: Bug report | 问题反馈
about: Tell us what problems you have encountered
title: "[BUG]"
labels: ''
name: 🐛 问题反馈 | Bug Report
about: 提交使用过程中遇到的问题 | Report an issue
title: "问题:"
labels: '🐛 bug'
assignees: ''
---
🙇‍♂️🙇‍♂️🙇‍♂️注意XrayR等非XBoard问题请前往项目提问
🙇‍♂️🙇‍♂️🙇‍♂️Note: XrayR and other non-XBoard issues please go to the project side to ask questions
<!-- 🔴 请注意XrayR等非XBoard问题请前往相应项目提问 -->
<!-- 🔴 Note: For XrayR and other non-XBoard issues, please report to their respective projects -->
> ⚠️ 请务必按照模板填写完整信息没有详细描述的issue可能会被忽略或关闭
> ⚠️ Please follow the template to provide complete information, issues without detailed description may be ignored or closed
**基本信息 | Basic Info**
```yaml
XBoard版本 | Version:
部署方式 | Deployment: [Docker/手动部署]
PHP版本 | Version:
数据库 | Database:
```
**问题描述 | Description**
<!-- 简要描述你遇到的问题 -->
The XBoard version number you are using
当前使用的XBoard版本号(git commit id)
--------
**复现步骤 | Steps**
<!-- 如何复现这个问题? -->
1.
2.
Would you like to deploy using Docker?
你的部署方式是否为Docker
--------
**相关截图 | Screenshots**
<!-- 拖拽图片到这里(请注意隐藏敏感信息)-->
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天的日志)。
--------
**日志信息 | Logs**
<!-- storage/logs 目录下的日志 -->
```log
// 粘贴日志内容到这里
```

View File

@@ -1,11 +1,28 @@
---
name: Feature request | 功能请求
about: Tell us what you need
title: "[Feature request]"
labels: ''
name: ✨ 功能请求 | Feature Request
about: 提交新功能建议或改进意见 | Suggest an idea
title: "建议:"
labels: '✨ enhancement'
assignees: ''
---
Please describe in detail the problems or needs you have encountered.
请详细描述你遇到的问题或需求。
> ⚠️ 请务必按照模板详细描述你的需求没有详细描述的issue可能会被忽略或关闭
> ⚠️ Please follow the template to describe your request in detail, issues without detailed description may be ignored or closed
**需求描述 | Description**
<!-- 描述你希望添加的功能或改进建议 -->
**使用场景 | Use Case**
<!-- 描述这个功能会在什么场景下使用,解决什么问题 -->
**功能建议 | Suggestion**
<!-- 你期望这个功能是什么样的?可以描述一下具体实现方式 -->
```yaml
功能形式 | Type: [新功能/功能优化/界面改进]
预期效果 | Expected:
```
**补充说明 | Additional**
<!-- 其他补充说明或者参考示例 -->

View File

@@ -1,89 +1,93 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Docker Build and Publish
on:
push:
branches: [ "dev" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
workflow_dispatch: # Enable manual trigger
branches: ["master"]
workflow_dispatch:
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- 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:master
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest
${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard
${{ 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: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.2.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Get version
id: get_version
run: echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.3.0
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:latest,${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard,${{ env.REGISTRY }}/${{ github.repository_owner }}/xboard:${{ steps.get_version.outputs.version }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
- name: Sign image
if: steps.build-and-push.outputs.digest != ''
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
COSIGN_EXPERIMENTAL: 1
run: |
echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes "{}@${{ steps.build-and-push.outputs.digest }}"

5
.gitignore vendored
View File

@@ -27,4 +27,7 @@ storage/laravels.pid
storage/laravels-timer-process.pid
cli-php.ini
frontend
docker-compose.yaml
docker-compose.yaml
bun.lockb
compose.yaml
.scribe

View File

@@ -1,17 +1,27 @@
FROM phpswoole/swoole:php8.1-alpine
FROM phpswoole/swoole:php8.2-alpine
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pcntl bcmath zip redis \
&& apk --no-cache add shadow sqlite mysql-client mysql-client mysql-dev mariadb-connector-c git patch supervisor redis \
&& addgroup -S -g 1000 www && adduser -S -G www -u 1000 www \
&& (getent group redis || addgroup -S redis) \
&& (getent passwd redis || adduser -S -G redis -H -h /data redis)
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 \
&& chown -R www:www /www \
&& chmod -R 775 /www
COPY .docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD /usr/bin/supervisord --nodaemon -c /etc/supervisor/supervisord.conf
RUN composer install --optimize-autoloader --no-cache --no-dev \
&& php artisan storage:link \
&& chown -R www:www /www \
&& chmod -R 775 /www \
&& mkdir -p /data \
&& chown redis:redis /data
ENV ENABLE_WEB=true \
ENABLE_HORIZON=true \
ENABLE_REDIS=false
EXPOSE 7001
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

129
README.md
View File

@@ -1,64 +1,91 @@
# 关于Xboard
Xboard是基于V2board二次开发在性能上和功能上都有大部分增强的**面板
# Xboard
# 免责声明
本项目只是本人个人学习开发并维护,本人不保证任何可用性,也不对使用本软件造成的任何后果负责。
<div align="center">
# Xboard 特点
基于V2board 二次开发,增加了以下特性
- 升级Laravel10
- 适配Laravels 提升至10+倍并发)
- 适配Webman 比laravels快50%左右)
- 修改配置从数据库中获取
- 支持Docker部署、分布式部署
- 支持根据用户IP归属地来下发订阅
- 增加Hy2支持
- 增加sing-box下发
- 支持直接从cloudflare获取访问者真实IP
- 支持根据客户端版本自动下发新协议
- 支持线路筛选(订阅地址后面增加 &filter=香港|美国)
- 支持Sqlite安装代替Mysql自用用户福音
- 使用Vue3 + TypeScript + NaiveUI + Unocss + Pinia重构用户前端
- 修复大量BUG
[![Telegram](https://img.shields.io/badge/Telegram-Channel-blue)](https://t.me/XboardOfficial)
![PHP](https://img.shields.io/badge/PHP-8.2+-green.svg)
![MySQL](https://img.shields.io/badge/MySQL-5.7+-blue.svg)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
# **系统架构**
English | [简体中文](README_CN.md)
- PHP8.1+
- Composer
- MySQL5.7+
- Redis
- Laravel
</div>
## 性能对比 [查看详情](./docs/性能对比.md)
> xboard 无论前端还是后端性能都有巨大的提升
## 📖 Introduction
|场景 | php-fpm(传统) | php-fpm(传统开启opcache) | laravels | webman(docker)|
|---- | ---- |---- |----| ---|
|首页 | 6请求/秒 | 157请求/秒 | 477请求/秒 | 803请求/秒 |
|用户订阅 | 6请求/秒 | 196请求/秒 | 586请求/秒 | 1064请求/秒 |
|用户首页延迟| 308ms | 110ms | 101ms | 98ms |
Xboard is a modern panel system built on Laravel 11, focusing on providing a clean and efficient user experience.
## 页面展示
![示例图片](./docs/images/dashboard.png)
## ✨ Features
## 安装 / 更新 / 回滚
你可以点击查看下列方式的**安装、更新**步骤
- [1panel 部署](./docs/1panel安装指南.md)
- [Docker Compose 纯命令行快速部署](./docs/docker-compose安装指南.md)
- [aapanel + Docker Compose (推荐)](./docs/aapanel+docker安装指南.md)
- [aapanel 部署](./docs/aapanel安装指南.md)
### 从其他版本迁移
#### 数据库迁移
**根据你的版本查看对应的迁移指南进行迁移**
- v2board dev 23/10/27的版本 [点击跳转迁移指南](./docs/v2b_dev迁移指南.md)
- v2board 1.7.4 [点击跳转迁移指南](./docs/v2b_1.7.4迁移指南.md)
- v2board 1.7.3 [点击跳转迁移指南](./docs/v2b_1.7.3迁移指南.md)
- v2board wyx2685 [点击跳转迁移指南](./docs/v2b_wyx2685迁移指南.md)
- 🚀 Built with Laravel 11 + Octane for significant performance gains
- 🎨 Redesigned admin interface (React + Shadcn UI)
- 📱 Modern user frontend (Vue3 + TypeScript)
- 🐳 Ready-to-use Docker deployment solution
- 🎯 Optimized system architecture for better maintainability
## 🚀 Quick Start
### 注意
> 修改后台路径需要重启才能生效
```bash
git clone -b compose --depth 1 https://github.com/cedar2025/Xboard && \
cd Xboard && \
docker compose run -it --rm \
-e ENABLE_SQLITE=true \
-e ENABLE_REDIS=true \
-e ADMIN_ACCOUNT=admin@demo.com \
web php artisan xboard:install && \
docker compose up -d
```
> After installation, visit: http://SERVER_IP:7001
> ⚠️ Make sure to save the admin credentials shown during installation
## 📖 Documentation
### 🔄 Upgrade Notice
> 🚨 **Important:** This version involves significant changes. Please strictly follow the upgrade documentation and backup your database before upgrading. Note that upgrading and migration are different processes, do not confuse them.
### Deployment Guides
- [Deploy with 1Panel](./docs/zh-CN/installation/1panel.md)
- [Deploy with Docker Compose](./docs/zh-CN/installation/docker-compose.md)
- [Deploy with aaPanel](./docs/zh-CN/installation/aapanel.md)
- [Deploy with aaPanel + Docker](./docs/zh-CN/installation/aapanel-docker.md) (Recommended)
### Migration Guides
- [Migrate from v2board dev](./docs/zh-CN/migration/v2board-dev.md)
- [Migrate from v2board 1.7.4](./docs/zh-CN/migration/v2board-1.7.4.md)
- [Migrate from v2board 1.7.3](./docs/zh-CN/migration/v2board-1.7.3.md)
- [Migrate from v2board wyx2685](./docs/zh-CN/migration/v2board-wyx2685.md)
## 🛠️ Tech Stack
- Backend: Laravel 11 + Octane
- Admin Panel: React + Shadcn UI + TailwindCSS
- User Frontend: Vue3 + TypeScript + NaiveUI
- Deployment: Docker + Docker Compose
- Caching: Redis + Octane Cache
## 📷 Preview
![Admin Preview](./docs/images/admin.png)
![User Preview](./docs/images/user.png)
## ⚠️ Disclaimer
This project is for learning and communication purposes only. Users are responsible for any consequences of using this project.
## 🔔 Important Notes
1. Restart required after modifying admin path:
```bash
docker compose restart
```
> 如果是是aapanel安装则需要重启 webman守护进程
2. For aaPanel installations, restart the Octane daemon process
## 🤝 Contributing
Issues and Pull Requests are welcome to help improve the project.
## 📈 Star History
[![Stargazers over time](https://starchart.cc/cedar2025/Xboard.svg)](https://starchart.cc/cedar2025/Xboard)

96
README_CN.md Normal file
View File

@@ -0,0 +1,96 @@
# Xboard
<div align="center">
[![Telegram](https://img.shields.io/badge/Telegram-Channel-blue)](https://t.me/XboardOfficial)
![PHP](https://img.shields.io/badge/PHP-8.2+-green.svg)
![MySQL](https://img.shields.io/badge/MySQL-5.7+-blue.svg)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[English](README.md) | 简体中文
</div>
## 📖 简介
Xboard 是一个基于 Laravel 11 开发的现代化面板系统,专注于提供简洁、高效的用户体验。
## ✨ 特性
- 🚀 基于 Laravel 11 + Octane性能提升显著
- 🎨 全新设计的管理界面 (React + Shadcn UI)
- 📱 现代化的用户前端 (Vue3 + TypeScript)
- 🐳 开箱即用的 Docker 部署方案
- 🎯 优化的系统架构,提供更好的可维护性
## 🚀 快速开始
```bash
git clone -b compose --depth 1 https://github.com/cedar2025/Xboard && \
cd Xboard && \
docker compose run -it --rm \
-e ENABLE_SQLITE=true \
-e ENABLE_REDIS=true \
-e ADMIN_ACCOUNT=admin@demo.com \
web php artisan xboard:install && \
docker compose up -d
```
> 安装完成后访问http://服务器IP:7001
> ⚠️ 请务必保存安装时显示的管理员账号密码
## 📚 使用文档
### 🔄 升级提示
> 🚨 **重要:** 此次版本跨度较大,请严格按照升级文档进行升级,必要时请备份数据库再进行升级。升级跟迁移不是一个东西,请不要混淆。
### 部署教程
- [使用 1Panel 部署](./docs/zh-CN/installation/1panel.md)
- [Docker Compose 部署](./docs/zh-CN/installation/docker-compose.md)
- [使用 aaPanel 部署](./docs/zh-CN/installation/aapanel.md)
- [aaPanel + Docker 部署](./docs/zh-CN/installation/aapanel-docker.md)(推荐)
### 迁移指南
- [从 v2board dev 迁移](./docs/zh-CN/migration/v2board-dev.md)
- [从 v2board 1.7.4 迁移](./docs/zh-CN/migration/v2board-1.7.4.md)
- [从 v2board 1.7.3 迁移](./docs/zh-CN/migration/v2board-1.7.3.md)
- [从 v2board wyx2685 迁移](./docs/zh-CN/migration/v2board-wyx2685.md)
## 🤝 参与贡献
欢迎提交 Issue 和 Pull Request 来帮助改进项目。
## 🛠️ 技术栈
- 后端Laravel 11 + Octane
- 管理面板React + Shadcn UI + TailwindCSS
- 用户前端Vue3 + TypeScript + NaiveUI
- 部署方案Docker + Docker Compose
- 缓存系统Redis + Octane Cache
## 📷 界面预览
![管理员后台](./docs/images/admin.png)
![用户前端](./docs/images/user.png)
## ⚠️ 免责声明
本项目仅供学习交流使用,使用本项目造成的任何后果由使用者自行承担。
## 🔔 注意事项
1. 修改后台路径后需要重启:
```bash
docker compose restart
```
2. aaPanel 环境下需要重启 Octane 守护进程
## 🤝 参与贡献
欢迎提交 Issue 和 Pull Request 来帮助改进项目。
## 📈 Star 增长趋势
[![Stargazers over time](https://starchart.cc/cedar2025/Xboard.svg)](https://starchart.cc/cedar2025/Xboard)

View File

@@ -4,6 +4,8 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Google\Cloud\Storage\StorageClient;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Process;
class BackupDatabase extends Command
@@ -85,14 +87,14 @@ class BackupDatabase extends Command
]);
// 输出文件链接
\Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
Log::channel('backup')->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
$this->info("🎉:数据库备份已上传到 Google Cloud Storage: $objectName");
\File::delete($compressedBackupPath);
File::delete($compressedBackupPath);
}
}catch(\Exception $e){
\Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
Log::channel('backup')->error("😔:数据库备份失败 \n" . $e);
$this->error("😔:数据库备份失败\n" . $e);
\File::delete($compressedBackupPath);
File::delete($compressedBackupPath);
}
}
}

View File

@@ -45,7 +45,7 @@ class ClearUser extends Command
->where('last_login_at', NULL);
$count = $builder->count();
if ($builder->delete()) {
$this->info("已删除${count}位没有任何数据的用户");
$this->info("已删除{$count}位没有任何数据的用户");
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class ExportV2Log extends Command
{
@@ -20,7 +21,7 @@ class ExportV2Log extends Command
$days = $this->argument('days');
$date = Carbon::now()->subDays($days)->startOfDay();
$logs = \DB::table('v2_log')
$logs = DB::table('v2_log')
->where('created_at', '>=', $date->timestamp)
->get();

View File

@@ -4,6 +4,8 @@ namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
class MigrateFromV2b extends Command
{
@@ -51,11 +53,12 @@ class MigrateFromV2b extends Command
],
'1.7.3' => [
'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;',
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount order_total INT COMMENT '订单合计';",
"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 paid_count INT NULL,
ADD COLUMN paid_total INT NULL,
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;
@@ -132,7 +135,7 @@ class MigrateFromV2b extends Command
try {
foreach ($sqlCommands[$version] as $sqlCommand) {
// Execute SQL command
\DB::statement($sqlCommand);
DB::statement($sqlCommand);
}
$this->info('1⃣、数据库差异矫正成功');
@@ -158,7 +161,7 @@ class MigrateFromV2b extends Command
public function MigrateV2ConfigToV2Settings()
{
\Artisan::call('config:clear');
Artisan::call('config:clear');
$configValue = config('v2board') ?? [];
foreach ($configValue as $k => $v) {
@@ -167,16 +170,16 @@ class MigrateFromV2b extends Command
// 如果记录不存在,则插入
if ($existingSetting) {
$this->warn("配置 ${k} 在数据库已经存在, 忽略");
$this->warn("配置 {$k} 在数据库已经存在, 忽略");
continue;
}
Setting::create([
'name' => $k,
'value' => is_array($v)? json_encode($v) : $v,
]);
$this->info("配置 ${k} 迁移成功");
$this->info("配置 {$k} 迁移成功");
}
\Artisan::call('config:cache');
Artisan::call('config:cache');
$this->info('所有配置迁移完成');
}

View File

@@ -39,11 +39,16 @@ class SendRemindMail extends Command
*/
public function handle()
{
if (!(bool) admin_setting('remind_mail_enable', false)) {
return;
}
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $mailService->remindExpire($user);
if ($user->remind_traffic) $mailService->remindTraffic($user);
if ($user->remind_expire)
$mailService->remindExpire($user);
if ($user->remind_traffic)
$mailService->remindTraffic($user);
}
}
}

View File

@@ -7,6 +7,10 @@ use Illuminate\Encryption\Encrypter;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\text;
use function Laravel\Prompts\note;
@@ -45,17 +49,17 @@ class XboardInstall extends Command
public function handle()
{
try {
$isDocker = env('docker', false);
$enableSqlite = env('enable_sqlite', false);
$enableRedis = env('enable_redis', false);
$adminAccount = env('admin_account', '');
$isDocker = file_exists('/.dockerenv');
$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'))
(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'))));
@@ -86,11 +90,11 @@ class XboardInstall extends Command
'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))) {
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]);
@@ -115,16 +119,16 @@ class XboardInstall extends Command
'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();
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 (!blank(DB::connection('mysql')->select('SHOW TABLES'))) {
if (confirm(label: '检测到数据库中已经存在数据,是否要清空数据库以便安装新的数据?', default: false, yes: '清空', no: '不清空')) {
$this->info('正在清空数据库请稍等');
$this->call('db:wipe', ['--force' => true]);
@@ -141,12 +145,11 @@ class XboardInstall extends Command
}
}
$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_HOST'] = '/data/redis.sock';
$envConfig['REDIS_PORT'] = 0;
$envConfig['REDIS_PASSWORD'] = null;
} else {
@@ -171,6 +174,8 @@ class XboardInstall extends Command
// 连接失败,输出错误消息
$this->error("redis连接失败" . $e->getMessage());
$this->info("请重新输入REDIS配置");
$enableRedis = false;
sleep(1);
}
}
@@ -191,10 +196,10 @@ class XboardInstall extends Command
$this->saveToEnv($envConfig);
$this->call('config:cache');
\Artisan::call('cache:clear');
Artisan::call('cache:clear');
$this->info('正在导入数据库请稍等...');
\Artisan::call("migrate", ['--force' => true]);
$this->info(\Artisan::output());
Artisan::call("migrate", ['--force' => true]);
$this->info(Artisan::output());
$this->info('数据库导入完成');
$this->info('开始注册管理员账号');
if (!$this->registerAdmin($email, $password)) {
@@ -206,6 +211,8 @@ class XboardInstall extends Command
$defaultSecurePath = hash('crc32b', config('app.key'));
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
$envConfig['INSTALLED'] = true;
$this->saveToEnv($envConfig);
} catch (\Exception $e) {
$this->error($e);
}

View File

@@ -44,8 +44,8 @@ class XboardStatistics extends Command
{
$startAt = microtime(true);
ini_set('memory_limit', -1);
$this->statUser();
$this->statServer();
// $this->statUser();
// $this->statServer();
$this->stat();
info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
}

View File

@@ -6,6 +6,7 @@ use App\Utils\CacheKey;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Cache;
use App\Services\UserOnlineService;
class Kernel extends ConsoleKernel
{
@@ -44,6 +45,10 @@ class Kernel extends ConsoleKernel
if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
$schedule->command('backup:database', ['true'])->daily()->onOneServer();
}
// 每分钟清理过期的在线状态
$schedule->call(function () {
app(UserOnlineService::class)->cleanExpiredOnlineStatus();
})->everyMinute();
}
/**

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Contracts;
interface PaymentInterface
{
public function form(): array;
public function pay($order): array;
public function notify($params);
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Contracts;
interface ProtocolInterface
{
public function getFlags(): array;
/**
* 处理并生成配置
*/
public function handle();
}

View File

@@ -1,15 +1,6 @@
<?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')) {
/**
* 获取或保存配置参数.

View File

@@ -1,141 +0,0 @@
<?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);
}
}

View File

@@ -1,73 +0,0 @@
<?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);
}
}

View File

@@ -1,186 +0,0 @@
<?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);
}
}

View File

@@ -1,75 +0,0 @@
<?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());
}
}

View File

@@ -1,113 +0,0 @@
<?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);
}
}

View File

@@ -1,45 +0,0 @@
<?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);
}
}

View File

@@ -1,84 +0,0 @@
<?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);
}
}

View File

@@ -1,76 +0,0 @@
<?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);
}
}

View File

@@ -1,122 +0,0 @@
<?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);
}
}

View File

@@ -1,79 +0,0 @@
<?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);
}
}

View File

@@ -1,206 +0,0 @@
<?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),
];
}
}

View File

@@ -1,81 +0,0 @@
<?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);
}
}

View File

@@ -1,92 +0,0 @@
<?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, '关闭失败']);
}
}
}

View File

@@ -14,7 +14,7 @@ class AppController extends Controller
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$user = $request->user();
$userService = new UserService();
if ($userService->isAvailable($user)) {
$servers = ServerService::getAvailableServers($user);
@@ -30,8 +30,9 @@ class AppController extends Controller
$proxies = [];
foreach ($servers as $item) {
$protocol_settings = $item['protocol_settings'];
if ($item['type'] === 'shadowsocks'
&& in_array($item['cipher'], [
&& in_array(data_get($protocol_settings, 'cipher'), [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',

View File

@@ -8,12 +8,27 @@ use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
class ClientController extends Controller
{
/**
* Protocol prefix mapping for server names
*/
private const PROTOCOL_PREFIXES = [
'hysteria' => [
1 => '[Hy]',
2 => '[Hy2]'
],
'vless' => '[vless]',
'shadowsocks' => '[ss]',
'vmess' => '[vmess]',
'trojan' => '[trojan]',
];
// 支持hy2 的客户端版本列表
const SupportedHy2ClientVersions = [
private const CLIENT_VERSIONS = [
'NekoBox' => '1.2.7',
'sing-box' => '1.5.0',
'stash' => '2.5.0',
@@ -26,121 +41,191 @@ class ClientController extends Controller
'loon' => '637',
'v2rayng' => '1.9.5',
'v2rayN' => '6.31',
'surge' => '2398'
'surge' => '2398',
'flclash' => '0.8.0'
];
// allowed types
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
private const ALLOWED_TYPES = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'hysteria2'];
/**
* 处理浏览器访问订阅的情况
*/
private function handleBrowserSubscribe($user, UserService $userService)
{
$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']) : __('Unlimited');
$resetDay = $userService->getResetDay($user);
// 获取通用订阅地址
$subscriptionUrl = Helper::getSubscribeUrl($user->token);
// 生成二维码
$writer = new \BaconQrCode\Writer(
new \BaconQrCode\Renderer\ImageRenderer(
new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200),
new \BaconQrCode\Renderer\Image\SvgImageBackEnd()
)
);
$qrCode = base64_encode($writer->writeString($subscriptionUrl));
$data = [
'username' => $user->email,
'status' => $userService->isAvailable($user) ? 'active' : 'inactive',
'data_limit' => $totalTraffic ? Helper::trafficConvert($totalTraffic) : '∞',
'data_used' => Helper::trafficConvert($useTraffic),
'expired_date' => $expiredDate,
'reset_day' => $resetDay,
'subscription_url' => $subscriptionUrl,
'qr_code' => $qrCode
];
// 只有当 device_limit 不为 null 时才添加到返回数据中
if ($user->device_limit !== null) {
$data['device_limit'] = $user->device_limit;
}
return response()->view('client.subscribe', $data);
}
/**
* 检查是否是浏览器访问
*/
private function isBrowserAccess(Request $request): bool
{
$userAgent = strtolower($request->input('flag', $request->header('User-Agent')));
return str_contains($userAgent, 'mozilla')
|| str_contains($userAgent, 'chrome')
|| str_contains($userAgent, 'safari')
|| str_contains($userAgent, 'edge');
}
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.
$request->validate([
'types' => ['nullable', 'string'],
'filter' => ['nullable', 'string'],
'flag' => ['nullable', 'string'],
]);
$user = $request->user();
$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();
if (!$userService->isAvailable($user)) {
return response()->json(['message' => 'Account unavailable'], 403);
}
// 检测是否是浏览器访问
if ($this->isBrowserAccess($request)) {
return $this->handleBrowserSubscribe($user, $userService);
}
$clientInfo = $this->getClientInfo($request);
$types = $this->getFilteredTypes($request->input('types'), $clientInfo['supportHy2']);
$filterArr = $this->getFilterArray($request->input('filter'));
// Get available servers and apply filters
$servers = ServerService::getAvailableServers($user);
$serversFiltered = $this->filterServers(
servers: $servers,
types: $types,
filters: $filterArr,
);
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
$serversFiltered = $this->addPrefixToServerName($serversFiltered);
// Handle protocol response
if ($clientInfo['flag']) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$className = 'App\\Protocols\\' . basename($file, '.php');
$protocol = new $className($user, $serversFiltered);
if (
collect($protocol->getFlags())
->contains(fn($f) => stripos($clientInfo['flag'], $f) !== false)
) {
return $protocol->handle();
}
}
}
return (new General($user, $serversFiltered))->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)
private function getFilteredTypes(string|null $types, bool $supportHy2): array
{
return collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2) {
if ($server['type'] == "hysteria" && $server['version'] == 2) {
if(!in_array('hysteria2', $typesArr)){
if ($types === 'all') {
return self::ALLOWED_TYPES;
}
$allowedTypes = $supportHy2
? self::ALLOWED_TYPES
: array_diff(self::ALLOWED_TYPES, ['hysteria2']);
if (!$types) {
return array_values($allowedTypes);
}
$userTypes = explode('|', str_replace(['|', '', ','], '|', $types));
return array_values(array_intersect($userTypes, $allowedTypes));
}
private function getFilterArray(?string $filter): ?array
{
if ($filter === null) {
return null;
}
return mb_strlen($filter) > 20 ? null :
explode('|', str_replace(['|', '', ','], '|', $filter));
}
private function getClientInfo(Request $request): array
{
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches);
$version = $matches[1] ?? null;
$supportHy2 = $version ? $this->checkHy2Support($flag, $version) : true;
return [
'flag' => $flag,
'version' => $version,
'supportHy2' => $supportHy2
];
}
private function checkHy2Support(string $flag, string $version): bool
{
$result = false;
foreach (self::CLIENT_VERSIONS as $client => $minVersion) {
if (stripos($flag, $client) !== false) {
$result = $result || version_compare($version, $minVersion, '>=');
}
}
return $result || !count(self::CLIENT_VERSIONS);
}
private function filterServers(array $servers, array $types, ?array $filters): array
{
return collect($servers)->reject(function ($server) use ($types, $filters) {
// Check Hysteria2 compatibility
if ($server['type'] === 'hysteria' && optional($server['protocol_settings'])['version'] === 2) {
if (!in_array('hysteria2', $types)) {
return true;
}elseif(false == $supportHy2){
}
} else {
if (!in_array($server['type'], $types)) {
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;
}
}
}
// Apply custom filters
if ($filters) {
return !collect($filters)->contains(function ($filter) use ($server) {
return stripos($server['name'], $filter) !== false
|| in_array($filter, $server['tags'] ?? []);
});
}
return false;
})->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
@@ -163,7 +248,7 @@ class ClientController extends Controller
$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']) : '长期有效';
$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], [
@@ -179,33 +264,41 @@ class ClientController extends Controller
]));
}
/**
* 判断版本号
* Add protocol prefix to server names if enabled in admin settings
*
* @param array<int, array<string, mixed>> $servers
* @return array<int, array<string, mixed>>
*/
function versionCompare($version1, $version2)
private function addPrefixToServerName(array $servers): array
{
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;
}
if (!admin_setting('show_protocol_to_server_enable', false)) {
return $servers;
}
// 版本号相等
return true;
return collect($servers)
->map(function (array $server): array {
$server['name'] = $this->getPrefixedServerName($server);
return $server;
})
->all();
}
/**
* Get server name with protocol prefix
*
* @param array<string, mixed> $server
*/
private function getPrefixedServerName(array $server): string
{
$type = $server['type'] ?? '';
if (!isset(self::PROTOCOL_PREFIXES[$type])) {
return $server['name'] ?? '';
}
$prefix = is_array(self::PROTOCOL_PREFIXES[$type])
? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
: self::PROTOCOL_PREFIXES[$type];
return $prefix . ($server['name'] ?? '');
}
}

View File

@@ -3,14 +3,23 @@
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Http\Resources\PlanResource;
use App\Models\Plan;
use App\Services\PlanService;
use Auth;
use Illuminate\Http\Request;
class PlanController extends Controller
{
protected $planService;
public function __construct(PlanService $planService)
{
$this->planService = $planService;
}
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return $this->success($plan);
$plan = $this->planService->getAvailablePlans();
return $this->success(PlanResource::collection($plan));
}
}

View File

@@ -23,7 +23,7 @@ class TelegramController extends Controller
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);
$data = json_decode(request()->getContent(),true);
$this->formatMessage($data);
$this->formatChatJoinRequest($data);
$this->handle();

View File

@@ -180,7 +180,7 @@ class AuthController extends Controller
$authService = new AuthService($user);
$data = $authService->generateAuthData($request);
$data = $authService->generateAuthData();
return $this->success($data);
}
@@ -223,48 +223,70 @@ class AuthController extends Controller
}
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
return $this->success($authService->generateAuthData());
}
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 ($token = $request->input('token')) {
$redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
return redirect()->to(
admin_setting('app_url')
? admin_setting('app_url') . $redirect
: url($redirect)
);
}
if ($request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
if ($verify = $request->input('verify')) {
$key = CacheKey::get('TEMP_TOKEN', $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 ')]);
return response()->json([
'message' => __('Token error')
], 400);
}
$user = User::findOrFail($userId);
if ($user->banned) {
return $this->fail([400,__('Your account has been suspended')]);
return response()->json([
'message' => __('Your account has been suspended')
], 400);
}
Cache::forget($key);
$authService = new AuthService($user);
return $this->success($authService->generateAuthData($request));
return response()->json([
'data' => $authService->generateAuthData()
]);
}
return response()->json([
'message' => __('Invalid request')
], 400);
}
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);
if (!$authorization) {
return response()->json([
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED
], 401);
}
$user = AuthService::findUserByBearerToken($authorization);
if (!$user) {
return response()->json([
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED
], 401);
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user['id'], 60);

View File

@@ -1,222 +0,0 @@
<?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;
}
}
}

View File

@@ -21,13 +21,9 @@ 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,'节点不存在']);
}
$server = $request->input('node_info');
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$users = ServerService::getAvailableUsers($server->group_id);
$users = ServerService::getAvailableUsers($server->group_ids);
$result = [];
foreach ($users as $user) {
array_push($result, [
@@ -49,15 +45,8 @@ class ShadowsocksTidalabController extends Controller
// 后端提交数据
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);
$server = $request->input('node_info');
$data = json_decode(request()->getContent(), 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();
@@ -73,4 +62,4 @@ class ShadowsocksTidalabController extends Controller
'msg' => 'ok'
]);
}
}
}

View File

@@ -23,9 +23,8 @@ class TrojanTidalabController extends Controller
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
@@ -51,15 +50,11 @@ class TrojanTidalabController extends Controller
// 后端提交数据
public function submit(Request $request)
{
$server = ServerTrojan::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
$data = get_request_content();
$data = json_decode($data, true);
$data = json_decode(request()->getContent(), 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();
@@ -78,6 +73,10 @@ class TrojanTidalabController extends Controller
// 后端获取配置
public function config(Request $request)
{
$server = $request->input('node_info');
if ($server->type !== 'trojan') {
return $this->fail([400, '节点不存在']);
}
$request->validate([
'node_id' => 'required',
'local_port' => 'required'
@@ -86,7 +85,7 @@ class TrojanTidalabController extends Controller
'local_port.required' => '本地端口不能为空'
]);
try {
$json = $this->getTrojanConfig($request->input('node_id'), $request->input('local_port'));
$json = $this->getTrojanConfig($server, $request->input('local_port'));
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '配置获取失败']);
@@ -95,19 +94,15 @@ class TrojanTidalabController extends Controller
return (json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
private function getTrojanConfig($server, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
return $this->fail([400, '节点不存在']);
}
$protocolSettings = $server->protocol_settings;
$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->sni = data_get($protocolSettings, '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;
}
}
}

View File

@@ -9,22 +9,30 @@ use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use App\Services\UserOnlineService;
use Illuminate\Http\JsonResponse;
class UniProxyController extends Controller
{
public function __construct(
private readonly UserOnlineService $userOnlineService
) {
}
// 后端获取用户
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();
$node = $request->input('node_info');
$nodeType = $node->type;
$nodeId = $node->id;
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
$users = ServerService::getAvailableUsers($node->group_ids);
$response['users'] = $users;
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
return response(null, 304);
}
@@ -34,131 +42,142 @@ class UniProxyController extends Controller
// 后端提交数据
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);
$res = json_decode(request()->getContent(), true);
if (!is_array($res)) {
return $this->fail([422, 'Invalid data format']);
}
$onlineUsers = $onlineCollection->all();
Cache::put($cacheKey, $onlineUsers, 3600);
$data = array_filter($res, function ($item) {
return is_array($item)
&& count($item) === 2
&& is_numeric($item[0])
&& is_numeric($item[1]);
});
if (empty($data)) {
return $this->success(true);
}
$node = $request->input('node_info');
$nodeType = $node->type;
$nodeId = $node->id;
Cache::put(
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
count($data),
3600
);
Cache::put(
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
time(),
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);
$userService->trafficFetch($node->toArray(), $nodeType, $data);
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
];
$node = $request->input('node_info');
$nodeType = $node->type;
$protocolSettings = $node->protocol_settings;
$serverPort = $node->server_port;
$host = $node->host;
$baseConfig = [
'server_port' => (int) $serverPort,
'network' => data_get($protocolSettings, 'network'),
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
];
$response = match ($nodeType) {
'shadowsocks' => [
...$baseConfig,
'cipher' => $protocolSettings['cipher'],
'obfs' => $protocolSettings['obfs'],
'obfs_settings' => $protocolSettings['obfs_settings'],
'server_key' => match ($protocolSettings['cipher']) {
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
default => null
}
],
'vmess' => [
...$baseConfig,
'tls' => (int) $protocolSettings['tls']
],
'trojan' => [
...$baseConfig,
'host' => $host,
'server_name' => $protocolSettings['server_name'],
],
'vless' => [
...$baseConfig,
'tls' => (int) $protocolSettings['tls'],
'flow' => $protocolSettings['flow'],
'tls_settings' =>
match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'],
default => $protocolSettings['tls_settings']
}
],
'hysteria' => [
'server_port' => (int) $serverPort,
'version' => (int) $protocolSettings['version'],
'host' => $host,
'server_name' => $protocolSettings['tls']['server_name'],
'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
...match ((int) $protocolSettings['version']) {
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
2 => [
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
'obfs-password' => $protocolSettings['obfs']['password'] ?? null
],
default => []
}
],
default => []
};
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);
if (!empty($node['route_ids'])) {
$response['routes'] = ServerService::getRoutes($node['route_ids']);
}
$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)
// 获取在线用户数据wyx2685
public function alivelist(Request $request): JsonResponse
{
return $this->success(true);
$node = $request->input('node_info');
$deviceLimitUsers = ServerService::getAvailableUsers($node->group_ids)
->where('device_limit', '>', 0);
$alive = $this->userOnlineService->getAliveList($deviceLimitUsers);
return response()->json(['alive' => (object) $alive]);
}
// 后端提交在线数据
public function alive(Request $request): JsonResponse
{
$node = $request->input('node_info');
$data = json_decode(request()->getContent(), true);
if ($data === null) {
return response()->json([
'error' => 'Invalid online data'
], 400);
}
$this->userOnlineService->updateAliveData($data, $node->type, $node->id);
return response()->json(['data' => true]);
}
}

View File

@@ -57,7 +57,7 @@ class TicketController extends Controller
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->user['id']
$request->user()->id
);
return $this->success(true);
}

View File

@@ -16,7 +16,7 @@ class CouponController extends Controller
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->user['id']);
$couponService->setUserId($request->user()->id);
$couponService->check();
return $this->success($couponService->getCoupon());
}

View File

@@ -17,11 +17,11 @@ 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)) {
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->user_id = $request->user()->id;
$inviteCode->code = Helper::randomChar(8);
return $this->success($inviteCode->save());
}
@@ -30,7 +30,7 @@ class InviteController extends Controller
{
$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'])
$builder = CommissionLog::where('invite_user_id', $request->user()->id)
->where('get_amount', '>', 0)
->orderBy('created_at', 'DESC');
$total = $builder->count();
@@ -45,7 +45,7 @@ class InviteController extends Controller
public function fetch(Request $request)
{
$commission_rate = admin_setting('invite_commission', 10);
$user = User::find($request->user['id'])
$user = User::find($request->user()->id)
->load(['codes' => fn($query) => $query->where('status', 0)]);
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;

View File

@@ -20,7 +20,7 @@ class KnowledgeController extends Controller
->first()
->toArray();
if (!$knowledge) return $this->fail([500, __('Article does not exist')]);
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
$userService = new UserService();
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);

View File

@@ -12,8 +12,8 @@ class NoticeController extends Controller
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC')
->where('show', 1);
$model = Notice::orderBy('sort', 'ASC')
->where('show', true);
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Http\Resources\OrderResource;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Plan;
@@ -13,6 +14,7 @@ use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\Plugin\HookManager;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
@@ -22,138 +24,127 @@ 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']));
$request->validate([
'status' => 'nullable|integer|in:0,1,2,3',
]);
$orders = Order::with('plan')
->where('user_id', $request->user()->id)
->when($request->input('status') !== null, function ($query) use ($request) {
$query->where('status', $request->input('status'));
})
->orderBy('created_at', 'DESC')
->get();
return $this->success(OrderResource::collection($orders));
}
public function detail(Request $request)
{
$order = Order::where('user_id', $request->user['id'])
$request->validate([
'trade_no' => 'required|string',
]);
$order = Order::with('payment')
->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');
$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);
return $this->success(OrderResource::make($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')]);
$request->validate([
'plan_id' => 'required|exists:App\Models\Plan,id',
'period' => 'required|string'
]);
$user = User::findOrFail($request->user()->id);
$userService = app(UserService::class);
if ($userService->isNotCompleteOrderByUserId($user->id)) {
throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
}
$planService = new PlanService($request->input('plan_id'));
$plan = Plan::findOrFail($request->input('plan_id'));
$planService = new PlanService($plan);
$plan = $planService->plan;
$user = User::find($request->user['id']);
// Validate plan purchase
$planService->validatePurchase($user, $request->input('period'));
if (!$plan) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
return DB::transaction(function () use ($request, $plan, $user, $userService, $planService) {
$period = $request->input('period');
$newPeriod = PlanService::getPeriodKey($period);
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')];
// Create order
$order = new Order([
'user_id' => $user->id,
'plan_id' => $plan->id,
'period' => $newPeriod,
'trade_no' => Helper::generateOrderNo(),
'total_amount' => optional($plan->prices)[$newPeriod] * 100
]);
// Apply coupon if provided
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();
$this->applyCoupon($order, $request->input('coupon_code'));
}
// Set order attributes
$orderService = new OrderService($order);
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
// Handle user balance
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;
}
$this->handleUserBalance($order, $user, $userService);
}
if (!$order->save()) {
DB::rollBack();
return $this->fail([400, __('Failed to create order')]);
throw new ApiException(__('Failed to create order'));
}
DB::commit();
}catch (\Exception $e){
DB::rollBack();
throw $e;
}
HookManager::call('order.after_create', $order);
return $this->success($order->trade_no);
return $this->success($order->trade_no);
});
}
protected function applyCoupon(Order $order, string $couponCode): void
{
$couponService = new CouponService($couponCode);
if (!$couponService->use($order)) {
throw new ApiException(__('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
protected function handleUserBalance(Order $order, User $user, UserService $userService): void
{
$remainingBalance = $user->balance - $order->total_amount;
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, -$order->total_amount)) {
throw new ApiException(__('Insufficient balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, -$user->balance)) {
throw new ApiException(__('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
public function checkout(Request $request)
@@ -161,7 +152,7 @@ class OrderController extends Controller
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->where('status', 0)
->first();
if (!$order) {
@@ -170,21 +161,24 @@ class OrderController extends Controller
// free process
if ($order->total_amount <= 0) {
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) return $this->fail([400, '支付失败']);
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')]);
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')]);
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,
@@ -201,7 +195,7 @@ class OrderController extends Controller
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);
@@ -232,7 +226,7 @@ class OrderController extends Controller
return $this->fail([422, __('Invalid parameter')]);
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$order) {
return $this->fail([400, __('Order does not exist')]);

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\V1\User;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Resources\PlanResource;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
@@ -11,29 +12,27 @@ use Illuminate\Http\Request;
class PlanController extends Controller
{
protected PlanService $planService;
public function __construct(PlanService $planService)
{
$this->planService = $planService;
}
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$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)) {
if (!$this->planService->isPlanAvailableForUser($plan, $user)) {
return $this->fail([400, __('Subscription plan does not exist')]);
}
return $this->success($plan);
return $this->success(PlanResource::make($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);
$plans = $this->planService->getAvailablePlans();
return $this->success(PlanResource::collection($plans));
}
}

View File

@@ -13,14 +13,14 @@ class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$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 ) {
if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
return response(null,304);
}
$data = NodeResource::collection($servers);

View File

@@ -15,23 +15,11 @@ class StatController extends Controller
{
$startDate = now()->startOfMonth()->timestamp;
$records = StatUser::query()
->where('user_id', $request->user['id'])
->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);
}

View File

@@ -21,6 +21,6 @@ class TelegramController extends Controller
public function unbind(Request $request)
{
$user = User::where('user_id', $request->user['id'])->first();
$user = User::where('user_id', $request->user()->id)->first();
}
}

View File

@@ -21,7 +21,7 @@ class TicketController extends Controller
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first()
->load('message');
if (!$ticket) {
@@ -33,7 +33,7 @@ class TicketController extends Controller
});
return $this->success(TicketResource::make($ticket)->additional(['message' => true]));
}
$ticket = Ticket::where('user_id', $request->user['id'])
$ticket = Ticket::where('user_id', $request->user()->id)
->orderBy('created_at', 'DESC')
->get();
return $this->success(TicketResource::collection($ticket));
@@ -43,20 +43,20 @@ class TicketController extends Controller
{
try{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->user['id'])->lockForUpdate()->count()) {
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']
'user_id' => $request->user()->id
]));
if (!$ticket) {
throw new \Exception(__('There are other unresolved tickets'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'user_id' => $request->user()->id,
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
@@ -64,7 +64,7 @@ class TicketController extends Controller
throw new \Exception(__('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
return $this->success(true);
}catch(\Exception $e){
DB::rollBack();
@@ -83,7 +83,7 @@ class TicketController extends Controller
return $this->fail([400, __('Message cannot be empty')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
@@ -91,18 +91,18 @@ class TicketController extends Controller
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) {
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']
$request->user()->id
)) {
return $this->fail([400, __('Ticket reply failed')]);
}
$this->sendNotify($ticket, $request->input('message'), $request->user['id']);
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
return $this->success(true);
}
@@ -113,7 +113,7 @@ class TicketController extends Controller
return $this->fail([422, __('Invalid parameter')]);
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->first();
if (!$ticket) {
return $this->fail([400, __('Ticket does not exist')]);
@@ -143,7 +143,7 @@ class TicketController extends Controller
)) {
return $this->fail([422, __('Unsupported withdrawal method')]);
}
$user = User::find($request->user['id']);
$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])]);
@@ -154,7 +154,7 @@ class TicketController extends Controller
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->user['id']
'user_id' => $request->user()->id
]);
if (!$ticket) {
return $this->fail([400, __('Failed to open ticket')]);
@@ -164,7 +164,7 @@ class TicketController extends Controller
__('Withdrawal account') . "" . $request->input('withdraw_account')
);
$ticketMessage = TicketMessage::create([
'user_id' => $request->user['id'],
'user_id' => $request->user()->id,
'ticket_id' => $ticket->id,
'message' => $message
]);
@@ -177,7 +177,7 @@ class TicketController extends Controller
DB::rollBack();
throw $e;
}
$this->sendNotify($ticket, $message, $request->user['id']);
$this->sendNotify($ticket, $message, $request->user()->id);
return $this->success(true);
}

View File

@@ -15,6 +15,7 @@ use App\Services\AuthService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@@ -22,7 +23,7 @@ class UserController extends Controller
{
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -32,7 +33,7 @@ class UserController extends Controller
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -43,9 +44,9 @@ class UserController extends Controller
public function checkLogin(Request $request)
{
$data = [
'is_login' => $request->user['id'] ? true : false
'is_login' => $request->user()?->id ? true : false
];
if ($request->user['is_admin']) {
if ($request->user()?->is_admin) {
$data['is_admin'] = true;
}
return $this->success($data);
@@ -53,7 +54,7 @@ class UserController extends Controller
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -76,7 +77,7 @@ class UserController extends Controller
public function info(Request $request)
{
$user = User::where('id', $request->user['id'])
$user = User::where('id', $request->user()->id)
->select([
'email',
'transfer_enable',
@@ -106,12 +107,12 @@ class UserController extends Controller
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->count(),
Ticket::where('status', 0)
->where('user_id', $request->user['id'])
->where('user_id', $request->user()->id)
->count(),
User::where('invite_user_id', $request->user['id'])
User::where('invite_user_id', $request->user()->id)
->count()
];
return $this->success($stat);
@@ -119,7 +120,7 @@ class UserController extends Controller
public function getSubscribe(Request $request)
{
$user = User::where('id', $request->user['id'])
$user = User::where('id', $request->user()->id)
->select([
'plan_id',
'token',
@@ -148,7 +149,7 @@ class UserController extends Controller
public function resetSecurity(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -167,7 +168,7 @@ class UserController extends Controller
'remind_traffic'
]);
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -182,7 +183,7 @@ class UserController extends Controller
public function transfer(UserTransfer $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}
@@ -199,7 +200,7 @@ class UserController extends Controller
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->user['id']);
$user = User::find($request->user()->id);
if (!$user) {
return $this->fail([400, __('The user does not exist')]);
}

View File

@@ -1,15 +1,15 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\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\Services\ThemeService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ConfigController extends Controller
{
@@ -34,7 +34,7 @@ class ConfigController extends Controller
public function testSendMail(Request $request)
{
$mailLog = MailService::sendEmail([
'email' => $request->user['email'],
'email' => $request->user()->email,
'subject' => 'This is xboard test email',
'template_name' => 'notify',
'template_value' => [
@@ -44,8 +44,7 @@ class ConfigController extends Controller
]
]);
return response([
'data' => true,
'log' => $mailLog
'data' => $mailLog,
]);
}
@@ -53,8 +52,9 @@ class ConfigController extends Controller
{
// 判断站点网址
$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([
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'));
@@ -68,45 +68,46 @@ class ConfigController extends Controller
$key = $request->input('key');
$data = [
'invite' => [
'invite_force' => (int)admin_setting('invite_force', 0),
'invite_force' => (bool) 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),
'invite_never_expire' => (bool) admin_setting('invite_never_expire', 0),
'commission_first_time_enable' => (bool) admin_setting('commission_first_time_enable', 1),
'commission_auto_check_enable' => (bool) 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),
'withdraw_close_enable' => (bool) admin_setting('withdraw_close_enable', 0),
'commission_distribution_enable' => (bool) 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),
'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),
'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),
'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
'reset_traffic_method' => (int) admin_setting('reset_traffic_method', 0),
'surplus_enable' => (bool) 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' => (bool) admin_setting('show_info_to_server_enable', 0),
'show_protocol_to_server_enable' => (bool) admin_setting('show_protocol_to_server_enable', 0),
'default_remind_expire' => (bool) admin_setting('default_remind_expire', 1),
'default_remind_traffic' => (bool) admin_setting('default_remind_traffic', 1),
'subscribe_path' => admin_setting('subscribe_path', 's'),
],
'frontend' => [
@@ -120,6 +121,7 @@ class ConfigController extends Controller
'server_token' => admin_setting('server_token'),
'server_pull_interval' => admin_setting('server_pull_interval', 60),
'server_push_interval' => admin_setting('server_push_interval', 60),
'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
],
'email' => [
'email_template' => admin_setting('email_template', 'default'),
@@ -128,35 +130,36 @@ class ConfigController extends Controller
'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')
'email_from_address' => admin_setting('email_from_address'),
'remind_mail_enable' => (bool) admin_setting('remind_mail_enable', false),
],
'telegram' => [
'telegram_bot_enable' => admin_setting('telegram_bot_enable', 0),
'telegram_bot_enable' => (bool) 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')
'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),
'email_verify' => (bool) admin_setting('email_verify', 0),
'safe_mode_enable' => (bool) 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_enable' => (bool) 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),
'email_gmail_limit_enable' => (bool) admin_setting('email_gmail_limit_enable', 0),
'recaptcha_enable' => (bool) admin_setting('recaptcha_enable', 0),
'recaptcha_key' => admin_setting('recaptcha_key', ''),
'recaptcha_site_key' => admin_setting('recaptcha_site_key', ''),
'register_limit_by_ip_enable' => (bool) 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_enable' => (bool) admin_setting('password_limit_enable', 1),
'password_limit_count' => admin_setting('password_limit_count', 5),
'password_limit_expire' => admin_setting('password_limit_expire', 60)
]
@@ -165,7 +168,8 @@ class ConfigController extends Controller
return $this->success([
$key => $data[$key]
]);
};
}
;
// TODO: default should be in Dict
return $this->success($data);
}
@@ -173,26 +177,13 @@ class ConfigController extends Controller
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]
);
foreach ($data as $k => $v) {
if ($k == 'frontend_theme') {
$themeService = new ThemeService();
$themeService->switch($v);
}
admin_setting([$k => $v]);
}
// 如果是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);
}

View File

@@ -0,0 +1,189 @@
<?php
namespace App\Http\Controllers\V2\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
{
private function applyFiltersAndSorts(Request $request, $builder)
{
if ($request->has('filter')) {
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$key = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($key, $value) {
if (is_array($value)) {
$query->whereIn($key, $value);
} else {
$query->where($key, 'like', "%{$value}%");
}
});
});
}
if ($request->has('sort')) {
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$key = $sort['id'];
$value = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($key, $value);
});
}
}
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$builder = Coupon::query();
$this->applyFiltersAndSorts($request, $builder);
$coupons = $builder
->orderBy('created_at', 'desc')
->paginate($pageSize, ["*"], 'page', $current);
return response([
'data' => $coupons->items(),
'total' => $coupons->total()
]);
}
public function update(Request $request)
{
$params = $request->validate([
'id' => 'required|numeric',
'show' => 'nullable|boolean'
], [
'id.required' => '优惠券ID不能为空',
'id.numeric' => '优惠券ID必须为数字'
]);
try {
DB::beginTransaction();
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
throw new ApiException(400201, '优惠券不存在');
}
$coupon->update($params);
DB::commit();
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '保存失败']);
}
}
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);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
@@ -16,12 +16,13 @@ class KnowledgeController extends Controller
{
if ($request->input('id')) {
$knowledge = Knowledge::find($request->input('id'))->toArray();
if (!$knowledge) return $this->fail([400202,'知识不存在']);
if (!$knowledge)
return $this->fail([400202, '知识不存在']);
return $this->success($knowledge);
}
$data = Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get();
->orderBy('sort', 'ASC')
->get();
return $this->success($data);
}
@@ -36,14 +37,14 @@ class KnowledgeController extends Controller
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
return $this->fail([500,'创建失败']);
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->fail([500, '创建失败']);
}
}
@@ -54,8 +55,8 @@ class KnowledgeController extends Controller
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
], [
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
@@ -69,11 +70,17 @@ class KnowledgeController extends Controller
return $this->success(true);
}
public function sort(KnowledgeSort $request)
public function sort(Request $request)
{
$request->validate([
'ids' => 'required|array'
], [
'ids.required' => '参数有误',
'ids.array' => '参数有误'
]);
try {
DB::beginTransaction();
foreach ($request->input('knowledge_ids') as $k => $v) {
foreach ($request->input('ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
@@ -90,15 +97,15 @@ class KnowledgeController extends Controller
{
$request->validate([
'id' => 'required|numeric'
],[
'id.required' => '知识库ID不能为空'
], [
'id.required' => '知识库ID不能为空'
]);
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
return $this->fail([400202,'知识不存在']);
return $this->fail([400202, '知识不存在']);
}
if (!$knowledge->delete()) {
return $this->fail([500,'删除失败']);
return $this->fail([500, '删除失败']);
}
return $this->success(true);

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return $this->success(
Notice::orderBy('sort', 'ASC')
->orderBy('id', 'DESC')
->get()
);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url',
'tags',
'show',
'popup'
]);
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);
}
public function sort(Request $request)
{
$params = $request->validate([
'ids' => 'required|array'
]);
try {
DB::beginTransaction();
foreach ($params['ids'] as $k => $v) {
$notice = Notice::findOrFail($v);
$notice->update(['sort' => $k + 1]);
}
DB::commit();
return $this->success(true);
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500, '排序保存失败']);
}
}
}

View File

@@ -0,0 +1,246 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\PlanService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
class OrderController extends Controller
{
public function detail(Request $request)
{
$order = Order::with(['user', 'plan', 'commission_log'])->find($request->input('id'));
if (!$order)
return $this->fail([400202, '订单不存在']);
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
$order['period'] = PlanService::getLegacyPeriod($order->period);
return $this->success($order);
}
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$orderModel = Order::with('plan:id,name');
if ($request->boolean('is_commission')) {
$orderModel->whereNotNull('invite_user_id')
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0);
}
$this->applyFiltersAndSorts($request, $orderModel);
return response()->json(
$orderModel
->latest('created_at')
->paginate(
perPage: $pageSize,
page: $current
)->through(fn($order) => [
...$order->toArray(),
'period' => PlanService::getLegacyPeriod($order->period)
]),
);
}
private function applyFiltersAndSorts(Request $request, Builder $builder): void
{
$this->applyFilters($request, $builder);
$this->applySorting($request, $builder);
}
private function applyFilters(Request $request, Builder $builder): void
{
if (!$request->has('filter')) {
return;
}
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$field = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($field, $value) {
$this->buildFilterQuery($query, $field, $value);
});
});
}
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
{
// Handle array values for 'in' operations
if (is_array($value)) {
$query->whereIn($field, $value);
return;
}
// Handle operator-based filtering
if (!is_string($value) || !str_contains($value, ':')) {
$query->where($field, 'like', "%{$value}%");
return;
}
[$operator, $filterValue] = explode(':', $value, 2);
// Convert numeric strings to appropriate type
if (is_numeric($filterValue)) {
$filterValue = strpos($filterValue, '.') !== false
? (float) $filterValue
: (int) $filterValue;
}
// Apply operator
$query->where($field, match (strtolower($operator)) {
'eq' => '=',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'like' => 'like',
'notlike' => 'not like',
'null' => static fn($q) => $q->whereNull($queryField),
'notnull' => static fn($q) => $q->whereNotNull($queryField),
default => 'like'
}, match (strtolower($operator)) {
'like', 'notlike' => "%{$filterValue}%",
'null', 'notnull' => null,
default => $filterValue
});
}
private function applySorting(Request $request, Builder $builder): void
{
if (!$request->has('sort')) {
return;
}
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$field = $sort['id'];
$direction = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($field, $direction);
});
}
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 = PlanService::getPeriodKey($request->input('period'));
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if (PlanService::getPeriodKey($order->period) === Plan::PERIOD_RESET_TRAFFIC) {
$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);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\Admin;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
@@ -38,7 +38,7 @@ class PaymentController extends Controller
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return $this->success($paymentService->form());
return $this->success(collect($paymentService->form())->values());
}
public function show(Request $request)

View File

@@ -1,16 +1,11 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\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;
@@ -18,24 +13,34 @@ 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;
}
}
$plans = Plan::orderBy('sort', 'ASC')
->with([
'group:id,name'
])
->withCount('users')
->get();
return $this->success($plans);
}
public function save(PlanSave $request)
public function save(Request $request)
{
$params = $request->validated();
$params = $request->validate([
'id' => 'nullable|integer',
'name' => 'required|string',
'content' => 'nullable|string',
'reset_traffic_method' => 'integer|nullable',
'transfer_enable' => 'integer|required',
'prices' => 'array|nullable',
'group_id' => 'integer|nullable',
'speed_limit' => 'integer|nullable',
'device_limit' => 'integer|nullable',
'capacity_limit' => 'integer|nullable',
]);
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
DB::beginTransaction();
// update user group id and transfer
@@ -44,7 +49,8 @@ class PlanController extends Controller
User::where('plan_id', $plan->id)->update([
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824,
'speed_limit' => $params['speed_limit']
'speed_limit' => $params['speed_limit'],
'device_limit' => $params['device_limit'],
]);
}
$plan->update($params);
@@ -53,11 +59,11 @@ class PlanController extends Controller
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
}
if (!Plan::create($params)) {
return $this->fail([500 ,'创建失败']);
return $this->fail([500, '创建失败']);
}
return $this->success(true);
}
@@ -65,57 +71,61 @@ class PlanController extends Controller
public function drop(Request $request)
{
if (Order::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在订单无法删除']);
return $this->fail([400201, '该订阅下存在订单无法删除']);
}
if (User::where('plan_id', $request->input('id'))->first()) {
return $this->fail([400201 ,'该订阅下存在用户无法删除']);
return $this->fail([400201, '该订阅下存在用户无法删除']);
}
if ($request->input('id')) {
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
}
return $this->success($plan->delete());
}
public function update(PlanUpdate $request)
public function update(Request $request)
{
$updateData = $request->only([
'show',
'renew'
'renew',
'sell'
]);
$plan = Plan::find($request->input('id'));
if (!$plan) {
return $this->fail([400202 ,'该订阅不存在']);
return $this->fail([400202, '该订阅不存在']);
}
try {
$plan->update($updateData);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success();
return $this->success(true);
}
public function sort(PlanSort $request)
public function sort(Request $request)
{
try{
$params = $request->validate([
'ids' => 'required|array'
]);
try {
DB::beginTransaction();
foreach ($request->input('plan_ids') as $k => $v) {
foreach ($params['ids'] as $k => $v) {
if (!Plan::find($v)->update(['sort' => $k + 1])) {
throw new \Exception();
}
}
DB::commit();
}catch (\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500 ,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}

View File

@@ -0,0 +1,195 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Plugin;
use App\Services\Plugin\PluginManager;
use App\Services\Plugin\PluginConfigService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class PluginController extends Controller
{
protected PluginManager $pluginManager;
protected PluginConfigService $configService;
public function __construct(
PluginManager $pluginManager,
PluginConfigService $configService
) {
$this->pluginManager = $pluginManager;
$this->configService = $configService;
}
/**
* 获取插件列表
*/
public function index()
{
$installedPlugins = Plugin::get()
->keyBy('code')
->toArray();
$pluginPath = base_path('plugins');
$plugins = [];
if (File::exists($pluginPath)) {
$directories = File::directories($pluginPath);
foreach ($directories as $directory) {
$pluginName = basename($directory);
$configFile = $directory . '/config.json';
if (File::exists($configFile)) {
$config = json_decode(File::get($configFile), true);
$installed = isset($installedPlugins[$pluginName]);
// 使用配置服务获取配置
$pluginConfig = $installed ? $this->configService->getConfig($pluginName) : ($config['config'] ?? []);
$plugins[] = [
'code' => $config['code'],
'name' => $config['name'],
'version' => $config['version'],
'description' => $config['description'],
'author' => $config['author'],
'is_installed' => $installed,
'is_enabled' => $installed ? $installedPlugins[$pluginName]['is_enabled'] : false,
'config' => $pluginConfig,
];
}
}
}
return response()->json([
'data' => $plugins
]);
}
/**
* 安装插件
*/
public function install(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->install($request->input('code'));
return response()->json([
'message' => '插件安装成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件安装失败:' . $e->getMessage()
], 400);
}
}
/**
* 卸载插件
*/
public function uninstall(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->uninstall($request->input('code'));
return response()->json([
'message' => '插件卸载成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件卸载失败:' . $e->getMessage()
], 400);
}
}
/**
* 启用插件
*/
public function enable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->enable($request->input('code'));
return response()->json([
'message' => '插件启用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件启用失败:' . $e->getMessage()
], 400);
}
}
/**
* 禁用插件
*/
public function disable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->disable($request->input('code'));
return response()->json([
'message' => '插件禁用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件禁用失败:' . $e->getMessage()
], 400);
}
}
/**
* 获取插件配置
*/
public function getConfig(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$config = $this->configService->getConfig($request->input('code'));
return response()->json([
'data' => $config
]);
} catch (\Exception $e) {
return response()->json([
'message' => '获取配置失败:' . $e->getMessage()
], 400);
}
}
/**
* 更新插件配置
*/
public function updateConfig(Request $request)
{
$request->validate([
'code' => 'required|string',
'config' => 'required|array'
]);
try {
$this->configService->updateConfig(
$request->input('code'),
$request->input('config')
);
return response()->json([
'message' => '配置更新成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '配置更新失败:' . $e->getMessage()
], 400);
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\V2\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class GroupController extends Controller
{
public function fetch(Request $request): JsonResponse
{
$serverGroups = ServerGroup::query()
->orderByDesc('id')
->withCount('users')
->get()
->transform(function ($group) {
$group->server_count = $group->servers()->count();
return $group;
});
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)
{
$groupId = $request->input('id');
$serverGroup = ServerGroup::find($groupId);
if (!$serverGroup) {
return $this->fail([400202, '组不存在']);
}
if (Server::whereJsonContains('group_ids', $groupId)->exists()) {
return $this->fail([400, '该组已被节点所使用,无法删除']);
}
if (Plan::where('group_id', $groupId)->exists()) {
return $this->fail([400, '该组已被订阅所使用,无法删除']);
}
if (User::where('group_id', $groupId)->exists()) {
return $this->fail([400, '该组已被用户所使用,无法删除']);
}
return $this->success($serverGroup->delete());
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\V2\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerSave;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
$servers = collect(ServerService::getAllServers())->map(function ($item) {
$item['groups'] = ServerGroup::whereIn('id', $item['group_ids'])->get(['name', 'id']);
$item['parent'] = $item->parent;
return $item;
});
return $this->success($servers);
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->validate([
'*.id' => 'numeric',
'*.order' => 'numeric'
]);
try {
DB::beginTransaction();
collect($params)->each(function ($item) {
if (isset($item['id']) && isset($item['order'])) {
Server::where('id', $item['id'])->update(['sort' => $item['order']]);
}
});
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
public function save(ServerSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = Server::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 {
Server::create($params);
return $this->success(true);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500, '创建失败']);
}
}
public function update(Request $request)
{
$request->validate([
'id' => 'required|integer',
'show' => 'integer',
]);
if (Server::where('id', $request->id)->update(['show' => $request->show]) === false) {
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
/**
* 删除
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function drop(Request $request)
{
$request->validate([
'id' => 'required|integer',
]);
if (Server::where('id', $request->id)->delete() === false) {
return $this->fail([500, '删除失败']);
}
return $this->success(true);
}
/**
* 复制节点
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server->show = 0;
$server->code = null;
if (!$server) {
return $this->fail([400202, '服务器不存在']);
}
Server::create($server->toArray());
return $this->success(true);
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
namespace App\Http\Controllers\V2\Admin\Server;
use App\Exceptions\ApiException;
use App\Http\Controllers\Controller;

View File

@@ -5,9 +5,7 @@ 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\Server;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
@@ -15,78 +13,497 @@ 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)
private $service;
public function __construct(StatisticalService $service)
{
$params = $request->validate([
'start_at' => '',
'end_at' => ''
$this->service = $service;
}
public function getOverride(Request $request)
{
// 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus();
return $server->is_online;
})->count();
// 获取在线设备数和在线用户数
$onlineDevices = User::where('t', '>=', time() - 600)
->sum('online_count');
$onlineUsers = User::where('t', '>=', time() - 600)
->count();
// 获取今日流量统计
$todayStart = strtotime('today');
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取本月流量统计
$monthStart = strtotime(date('Y-m-1'));
$monthTraffic = StatServer::where('record_at', '>=', $monthStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取总流量统计
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
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'),
// 新增统计数据
'online_nodes' => $onlineNodes,
'online_devices' => $onlineDevices,
'online_users' => $onlineUsers,
'today_traffic' => [
'upload' => $todayTraffic->upload ?? 0,
'download' => $todayTraffic->download ?? 0,
'total' => $todayTraffic->total ?? 0
],
'month_traffic' => [
'upload' => $monthTraffic->upload ?? 0,
'download' => $monthTraffic->download ?? 0,
'total' => $monthTraffic->total ?? 0
],
'total_traffic' => [
'upload' => $totalTraffic->upload ?? 0,
'download' => $totalTraffic->download ?? 0,
'total' => $totalTraffic->total ?? 0
]
]
];
}
/**
* Get order statistics with filtering and pagination
*
* @param Request $request
* @return array
*/
public function getOrder(Request $request)
{
$request->validate([
'start_date' => 'nullable|date_format:Y-m-d',
'end_date' => 'nullable|date_format:Y-m-d',
'type' => 'nullable|in:paid_total,paid_count,commission_total,commission_count',
]);
if (isset($params['start_at']) && isset($params['end_at'])) {
$stats = Stat::where('record_at', '>=', $params['start_at'])
->where('record_at', '<', $params['end_at'])
$query = Stat::where('record_type', 'd');
// Apply date filters
if ($request->input('start_date')) {
$query->where('record_at', '>=', strtotime($request->input('start_date')));
}
if ($request->input('end_date')) {
$query->where('record_at', '<=', strtotime($request->input('end_date') . ' 23:59:59'));
}
$statistics = $query->orderBy('record_at', 'DESC')
->get();
$summary = [
'paid_total' => 0,
'paid_count' => 0,
'commission_total' => 0,
'commission_count' => 0,
'start_date' => $request->input('start_date', date('Y-m-d', $statistics->last()?->record_at)),
'end_date' => $request->input('end_date', date('Y-m-d', $statistics->first()?->record_at)),
'avg_paid_amount' => 0,
'avg_commission_amount' => 0
];
$dailyStats = [];
foreach ($statistics as $statistic) {
$date = date('Y-m-d', $statistic['record_at']);
// Update summary
$summary['paid_total'] += $statistic['paid_total'];
$summary['paid_count'] += $statistic['paid_count'];
$summary['commission_total'] += $statistic['commission_total'];
$summary['commission_count'] += $statistic['commission_count'];
// Calculate daily stats
$dailyData = [
'date' => $date,
'paid_total' => $statistic['paid_total'],
'paid_count' => $statistic['paid_count'],
'commission_total' => $statistic['commission_total'],
'commission_count' => $statistic['commission_count'],
'avg_order_amount' => $statistic['paid_count'] > 0 ? round($statistic['paid_total'] / $statistic['paid_count'], 2) : 0,
'avg_commission_amount' => $statistic['commission_count'] > 0 ? round($statistic['commission_total'] / $statistic['commission_count'], 2) : 0
];
if ($request->input('type')) {
$dailyStats[] = [
'date' => $date,
'value' => $statistic[$request->input('type')],
'type' => $this->getTypeLabel($request->input('type'))
];
} else {
$dailyStats[] = $dailyData;
}
}
// Calculate averages for summary
if ($summary['paid_count'] > 0) {
$summary['avg_paid_amount'] = round($summary['paid_total'] / $summary['paid_count'], 2);
}
if ($summary['commission_count'] > 0) {
$summary['avg_commission_amount'] = round($summary['commission_total'] / $summary['commission_count'], 2);
}
// Add percentage calculations to summary
$summary['commission_rate'] = $summary['paid_total'] > 0
? round(($summary['commission_total'] / $summary['paid_total']) * 100, 2)
: 0;
return [
'code' => 0,
'message' => 'success',
'data' => [
'list' => array_reverse($dailyStats),
'summary' => $summary,
]
];
}
/**
* Get human readable label for statistic type
*
* @param string $type
* @return string
*/
private function getTypeLabel(string $type): string
{
return match ($type) {
'paid_total' => '收款金额',
'paid_count' => '收款笔数',
'commission_total' => '佣金金额(已发放)',
'commission_count' => '佣金笔数(已发放)',
default => $type
};
}
// 获取当日实时流量排行
public function getServerLastRank()
{
$data = $this->service->getServerRank();
return $this->success(data: $data);
}
// 获取昨日节点流量排行
public function getServerYesterdayRank()
{
$data = $this->service->getServerRank('yesterday');
return $this->success($data);
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$pageSize = $request->input('pageSize', 10);
$records = StatUser::orderBy('record_at', 'DESC')
->where('user_id', $request->input('user_id'))
->paginate($pageSize);
$data = $records->items();
return [
'data' => $data,
'total' => $records->total(),
];
}
public function getStatRecord(Request $request)
{
return [
'data' => $this->service->getStatRecord($request->input('type'))
];
}
/**
* Get comprehensive statistics data including income, users, and growth rates
*/
public function getStats()
{
$currentMonthStart = strtotime(date('Y-m-01'));
$lastMonthStart = strtotime('-1 month', $currentMonthStart);
$twoMonthsAgoStart = strtotime('-2 month', $currentMonthStart);
// Today's start timestamp
$todayStart = strtotime('today');
$yesterdayStart = strtotime('-1 day', $todayStart);
// 获取在线节点数
$onlineNodes = Server::all()->filter(function ($server) {
$server->loadServerStatus();
return $server->is_online;
})->count();
// 获取在线设备数和在线用户数
$onlineDevices = User::where('t', '>=', time() - 600)
->sum('online_count');
$onlineUsers = User::where('t', '>=', time() - 600)
->count();
// 获取今日流量统计
$todayTraffic = StatServer::where('record_at', '>=', $todayStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取本月流量统计
$monthTraffic = StatServer::where('record_at', '>=', $currentMonthStart)
->where('record_at', '<', time())
->selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// 获取总流量统计
$totalTraffic = StatServer::selectRaw('SUM(u) as upload, SUM(d) as download, SUM(u + d) as total')
->first();
// Today's income
$todayIncome = Order::where('created_at', '>=', $todayStart)
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Yesterday's income for day growth calculation
$yesterdayIncome = Order::where('created_at', '>=', $yesterdayStart)
->where('created_at', '<', $todayStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Current month income
$currentMonthIncome = Order::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Last month income
$lastMonthIncome = Order::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Last month commission payout
$lastMonthCommissionPayout = CommissionLog::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->sum('get_amount');
// Current month commission payout
$currentMonthCommissionPayout = CommissionLog::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->sum('get_amount');
// Current month new users
$currentMonthNewUsers = User::where('created_at', '>=', $currentMonthStart)
->where('created_at', '<', time())
->count();
// Total users
$totalUsers = User::count();
// Active users (users with valid subscription)
$activeUsers = User::where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
})->count();
// Previous month income for growth calculation
$twoMonthsAgoIncome = Order::where('created_at', '>=', $twoMonthsAgoStart)
->where('created_at', '<', $lastMonthStart)
->whereNotIn('status', [0, 2])
->sum('total_amount');
// Previous month commission for growth calculation
$twoMonthsAgoCommission = CommissionLog::where('created_at', '>=', $twoMonthsAgoStart)
->where('created_at', '<', $lastMonthStart)
->sum('get_amount');
// Previous month users for growth calculation
$lastMonthNewUsers = User::where('created_at', '>=', $lastMonthStart)
->where('created_at', '<', $currentMonthStart)
->count();
// Calculate growth rates
$monthIncomeGrowth = $lastMonthIncome > 0 ? round(($currentMonthIncome - $lastMonthIncome) / $lastMonthIncome * 100, 1) : 0;
$lastMonthIncomeGrowth = $twoMonthsAgoIncome > 0 ? round(($lastMonthIncome - $twoMonthsAgoIncome) / $twoMonthsAgoIncome * 100, 1) : 0;
$commissionGrowth = $twoMonthsAgoCommission > 0 ? round(($lastMonthCommissionPayout - $twoMonthsAgoCommission) / $twoMonthsAgoCommission * 100, 1) : 0;
$userGrowth = $lastMonthNewUsers > 0 ? round(($currentMonthNewUsers - $lastMonthNewUsers) / $lastMonthNewUsers * 100, 1) : 0;
$dayIncomeGrowth = $yesterdayIncome > 0 ? round(($todayIncome - $yesterdayIncome) / $yesterdayIncome * 100, 1) : 0;
// 获取待处理工单和佣金数据
$ticketPendingTotal = Ticket::where('status', 0)->count();
$commissionPendingTotal = Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->whereIn('status', [Order::STATUS_COMPLETED])
->where('commission_balance', '>', 0)
->count();
return [
'data' => [
// 收入相关
'todayIncome' => $todayIncome,
'dayIncomeGrowth' => $dayIncomeGrowth,
'currentMonthIncome' => $currentMonthIncome,
'lastMonthIncome' => $lastMonthIncome,
'monthIncomeGrowth' => $monthIncomeGrowth,
'lastMonthIncomeGrowth' => $lastMonthIncomeGrowth,
// 佣金相关
'currentMonthCommissionPayout' => $currentMonthCommissionPayout,
'lastMonthCommissionPayout' => $lastMonthCommissionPayout,
'commissionGrowth' => $commissionGrowth,
'commissionPendingTotal' => $commissionPendingTotal,
// 用户相关
'currentMonthNewUsers' => $currentMonthNewUsers,
'totalUsers' => $totalUsers,
'activeUsers' => $activeUsers,
'userGrowth' => $userGrowth,
'onlineUsers' => $onlineUsers,
'onlineDevices' => $onlineDevices,
// 工单相关
'ticketPendingTotal' => $ticketPendingTotal,
// 节点相关
'onlineNodes' => $onlineNodes,
// 流量统计
'todayTraffic' => [
'upload' => $todayTraffic->upload ?? 0,
'download' => $todayTraffic->download ?? 0,
'total' => $todayTraffic->total ?? 0
],
'monthTraffic' => [
'upload' => $monthTraffic->upload ?? 0,
'download' => $monthTraffic->download ?? 0,
'total' => $monthTraffic->total ?? 0
],
'totalTraffic' => [
'upload' => $totalTraffic->upload ?? 0,
'download' => $totalTraffic->download ?? 0,
'total' => $totalTraffic->total ?? 0
]
]
];
}
/**
* Get traffic ranking data for nodes or users
*
* @param Request $request
* @return array
*/
public function getTrafficRank(Request $request)
{
$request->validate([
'type' => 'required|in:node,user',
'start_time' => 'nullable|integer|min:1000000000|max:9999999999',
'end_time' => 'nullable|integer|min:1000000000|max:9999999999'
]);
$type = $request->input('type');
$startDate = $request->input('start_time', strtotime('-7 days'));
$endDate = $request->input('end_time', time());
$previousStartDate = $startDate - ($endDate - $startDate);
$previousEndDate = $startDate;
if ($type === 'node') {
// Get node traffic data
$currentData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
->where('record_at', '>=', $startDate)
->where('record_at', '<=', $endDate)
->groupBy('server_id')
->orderBy('value', 'DESC')
->limit(10)
->get();
// Get previous period data for comparison
$previousData = StatServer::selectRaw('server_id as id, SUM(u + d) as value')
->where('record_at', '>=', $previousStartDate)
->where('record_at', '<', $previousEndDate)
->whereIn('server_id', $currentData->pluck('id'))
->groupBy('server_id')
->get()
->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
->toArray();
->keyBy('id');
} else {
$statisticalService = new StatisticalService();
return [
'data' => $statisticalService->generateStatData()
// Get user traffic data
$currentData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
->where('record_at', '>=', $startDate)
->where('record_at', '<=', $endDate)
->groupBy('user_id')
->orderBy('value', 'DESC')
->limit(10)
->get();
// Get previous period data for comparison
$previousData = StatUser::selectRaw('user_id as id, SUM(u + d) as value')
->where('record_at', '>=', $previousStartDate)
->where('record_at', '<', $previousEndDate)
->whereIn('user_id', $currentData->pluck('id'))
->groupBy('user_id')
->get()
->keyBy('id');
}
$result = [];
foreach ($currentData as $data) {
$previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
$change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
$name = $type === 'node'
? optional(Server::find($data->id))->name ?? "Node {$data->id}"
: optional(User::find($data->id))->email ?? "User {$data->id}";
$result[] = [
'id' => (string) $data->id,
'name' => $name,
'value' => $data->value, // Convert to GB
'previousValue' => $previousValue, // Convert to GB
'change' => $change,
'timestamp' => date('c', $endDate)
];
}
$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)
'timestamp' => date('c'),
'data' => $result
];
}
}

View File

@@ -1,14 +1,12 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\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;

View File

@@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\V2\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 $themeService;
public function __construct(ThemeService $themeService)
{
$this->themeService = $themeService;
}
/**
* 上传新主题
*
* @throws ApiException
*/
public function upload(Request $request)
{
$request->validate([
'file' => [
'required',
'file',
'mimes:zip',
'max:10240', // 最大10MB
]
], [
'file.required' => '请选择主题包文件',
'file.file' => '无效的文件类型',
'file.mimes' => '主题包必须是zip格式',
'file.max' => '主题包大小不能超过10MB'
]);
try {
// 检查上传目录权限
$uploadPath = storage_path('tmp');
if (!File::exists($uploadPath)) {
File::makeDirectory($uploadPath, 0755, true);
}
if (!is_writable($uploadPath)) {
throw new ApiException('上传目录无写入权限');
}
// 检查主题目录权限
$themePath = base_path('theme');
if (!is_writable($themePath)) {
throw new ApiException('主题目录无写入权限');
}
$file = $request->file('file');
// 检查文件MIME类型
$mimeType = $file->getMimeType();
if (!in_array($mimeType, ['application/zip', 'application/x-zip-compressed'])) {
throw new ApiException('无效的文件类型仅支持ZIP格式');
}
// 检查文件名安全性
$originalName = $file->getClientOriginalName();
if (!preg_match('/^[a-zA-Z0-9\-\_\.]+\.zip$/', $originalName)) {
throw new ApiException('主题包文件名只能包含字母、数字、下划线、中划线和点');
}
$this->themeService->upload($file);
return $this->success(true);
} catch (ApiException $e) {
throw $e;
} catch (\Exception $e) {
\Log::error('Theme upload failed', [
'error' => $e->getMessage(),
'file' => $request->file('file')?->getClientOriginalName()
]);
throw new ApiException('主题上传失败:' . $e->getMessage());
}
}
/**
* 删除主题
*/
public function delete(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$this->themeService->delete($payload['name']);
return $this->success(true);
}
/**
* 获取所有主题和其配置列
*
* @return \Illuminate\Http\JsonResponse
*/
public function getThemes()
{
$data = [
'themes' => $this->themeService->getList(),
'active' => admin_setting('frontend_theme', 'Xboard')
];
return $this->success($data);
}
/**
* 切换主题
*/
public function switchTheme(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$this->themeService->switch($payload['name']);
return $this->success(true);
}
/**
* 获取主题配置
*/
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required'
]);
$data = $this->themeService->getConfig($payload['name']);
return $this->success($data);
}
/**
* 保存主题配置
*/
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required',
'config' => 'required'
]);
$this->themeService->updateConfig($payload['name'], $payload['config']);
$config = $this->themeService->getConfig($payload['name']);
return $this->success($config);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Services\TicketService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
class TicketController extends Controller
{
private function applyFiltersAndSorts(Request $request, $builder)
{
if ($request->has('filter')) {
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$key = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($key, $value) {
if (is_array($value)) {
$query->whereIn($key, $value);
} else {
$query->where($key, 'like', "%{$value}%");
}
});
});
}
if ($request->has('sort')) {
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$key = $sort['id'];
$value = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($key, $value);
});
}
}
public function fetch(Request $request)
{
if ($request->input('id')) {
return $this->fetchTicketById($request);
} else {
return $this->fetchTickets($request);
}
}
/**
* Summary of fetchTicketById
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
private function fetchTicketById(Request $request)
{
$ticket = Ticket::with('messages', 'user')->find($request->input('id'));
if (!$ticket) {
return $this->fail([400202, '工单不存在']);
}
$ticket->messages->each(function ($message) use ($ticket) {
$message->is_me = $message->user_id !== $ticket->user_id;
});
return $this->success($ticket);
}
/**
* Summary of fetchTickets
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
private function fetchTickets(Request $request)
{
$ticketModel = Ticket::query()
->when($request->has('status'), function ($query) use ($request) {
$query->where('status', $request->input('status'));
})
->when($request->has('reply_status'), function ($query) use ($request) {
$query->whereIn('reply_status', $request->input('reply_status'));
})
->when($request->has('email'), function ($query) use ($request) {
$query->whereHas('user', function ($q) use ($request) {
$q->where('email', $request->input('email'));
});
});
$this->applyFiltersAndSorts($request, $ticketModel);
return response()->json($ticketModel
->latest('updated_at')
->paginate(
perPage: $request->integer('pageSize', 10),
page: $request->integer('current', 1)
));
}
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, '关闭失败']);
}
}
}

View File

@@ -1,10 +1,8 @@
<?php
namespace App\Http\Controllers\V1\Admin;
namespace App\Http\Controllers\V2\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;
@@ -13,6 +11,7 @@ use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\Helper;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -21,71 +20,173 @@ class UserController extends Controller
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) return $this->fail([400202,'用户不存在']);
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)
/**
* Apply filters and sorts to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFiltersAndSorts(Request $request, Builder $builder): void
{
$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']);
}
}
$this->applyFilters($request, $builder);
$this->applySorting($request, $builder);
}
public function fetch(UserFetch $request)
/**
* Apply filters to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applyFilters(Request $request, Builder $builder): void
{
$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']);
if (!$request->has('filter')) {
return;
}
collect($request->input('filter'))->each(function ($filter) use ($builder) {
$field = $filter['id'];
$value = $filter['value'];
$builder->where(function ($query) use ($field, $value) {
$this->buildFilterQuery($query, $field, $value);
});
});
}
/**
* Build the filter query based on field and value
*
* @param Builder $query
* @param string $field
* @param mixed $value
* @return void
*/
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
{
// Handle array values for 'in' operations
if (is_array($value)) {
$query->whereIn($field === 'group_ids' ? 'group_id' : $field, $value);
return;
}
// Handle operator-based filtering
if (!is_string($value) || !str_contains($value, ':')) {
$query->where($field, 'like', "%{$value}%");
return;
}
[$operator, $filterValue] = explode(':', $value, 2);
// Convert numeric strings to appropriate type
if (is_numeric($filterValue)) {
$filterValue = strpos($filterValue, '.') !== false
? (float) $filterValue
: (int) $filterValue;
}
// Handle computed fields
$queryField = match ($field) {
'total_used' => DB::raw('(u + d)'),
default => $field
};
// Apply operator
$query->where($queryField, match (strtolower($operator)) {
'eq' => '=',
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'like' => 'like',
'notlike' => 'not like',
'null' => static fn($q) => $q->whereNull($queryField),
'notnull' => static fn($q) => $q->whereNotNull($queryField),
default => 'like'
}, match (strtolower($operator)) {
'like', 'notlike' => "%{$filterValue}%",
'null', 'notnull' => null,
default => $filterValue
});
}
/**
* Apply sorting to the query builder
*
* @param Request $request
* @param Builder $builder
* @return void
*/
private function applySorting(Request $request, Builder $builder): void
{
if (!$request->has('sort')) {
return;
}
collect($request->input('sort'))->each(function ($sort) use ($builder) {
$field = $sort['id'];
$direction = $sort['desc'] ? 'DESC' : 'ASC';
$builder->orderBy($field, $direction);
});
}
/**
* Fetch paginated user list with filters and sorting
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function fetch(Request $request)
{
$current = $request->input('current', 1);
$pageSize = $request->input('pageSize', 10);
$userModel = User::with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
->select(DB::raw('*, (u+d) as total_used'));
$this->applyFiltersAndSorts($request, $userModel);
$users = $userModel->orderBy('id', 'desc')
->paginate($pageSize, ['*'], 'page', $current);
$users->getCollection()->transform(function ($user) {
return $this->transformUserData($user);
});
return response([
'data' => $res,
'total' => $total
'data' => $users->items(),
'total' => $users->total()
]);
}
/**
* Transform user data for response
*
* @param User $user
* @return User
*/
private function transformUserData(User $user): User
{
$user->subscribe_url = Helper::getSubscribeUrl($user->token);
$user->balance = $user->balance / 100;
$user->commission_balance = $user->commission_balance / 100;
return $user;
}
public function getUserInfoById(Request $request)
{
$request->validate([
'id'=> 'required|numeric'
],[
'id' => 'required|numeric'
], [
'id.required' => '用户ID不能为空'
]);
$user = User::find($request->input('id'))->load('invite_user');
@@ -117,6 +218,7 @@ class UserController extends Controller
if (!$plan) {
return $this->fail([400202, '订阅计划不存在']);
}
// return json_encode($plan);
$params['group_id'] = $plan->group_id;
}
// 处理邀请用户
@@ -126,16 +228,22 @@ class UserController extends Controller
$params['invite_user_id'] = null;
}
if (isset($params['banned']) && (int)$params['banned'] === 1) {
if (isset($params['banned']) && (int) $params['banned'] === 1) {
$authService = new AuthService($user);
$authService->removeAllSession();
$authService->removeSession();
}
if (isset($params['balance'])) {
$params['balance'] = $params['balance'] * 100;
}
if (isset($params['commission_balance'])) {
$params['commission_balance'] = $params['commission_balance'] * 100;
}
try {
$user->update($params);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'保存失败']);
return $this->fail([500, '保存失败']);
}
return $this->success(true);
}
@@ -155,14 +263,14 @@ class UserController extends Controller
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
foreach($res as $user) {
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']);
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
@@ -174,7 +282,7 @@ class UserController extends Controller
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202,'订阅计划不存在']);
return $this->fail([400202, '订阅计划不存在']);
}
}
$user = [
@@ -187,11 +295,11 @@ class UserController extends Controller
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
return $this->fail([400201,'邮箱已存在于系统中']);
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->fail([500, '生成失败']);
}
return $this->success(true);
}
@@ -205,11 +313,11 @@ class UserController extends Controller
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
return $this->fail([400202,'订阅计划不存在']);
return $this->fail([400202, '订阅计划不存在']);
}
}
$users = [];
for ($i = 0;$i < $request->input('generate_count');$i++) {
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,
@@ -224,23 +332,23 @@ class UserController extends Controller
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
try{
try {
DB::beginTransaction();
if (!User::insert($users)) {
throw new \Exception();
}
DB::commit();
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
\Log::error($e);
return $this->fail([500,'生成失败']);
return $this->fail([500, '生成失败']);
}
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
foreach($users as $user) {
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']);
$subscribeUrl = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
@@ -254,17 +362,19 @@ class UserController extends Controller
$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');
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);
@@ -282,7 +392,7 @@ class UserController extends Controller
]);
} catch (\Exception $e) {
\Log::error($e);
return $this->fail([500,'处理失败']);
return $this->fail([500, '处理失败']);
}
return $this->success(true);

View File

@@ -29,7 +29,7 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
// \App\Http\Middleware\EncryptCookies::class,
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
@@ -39,9 +39,12 @@ class Kernel extends HttpKernel
],
'api' => [
// \App\Http\Middleware\EncryptCookies::class,
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
// \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\Language::class,
'bindings',
@@ -56,6 +59,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
@@ -70,6 +74,8 @@ class Kernel extends HttpKernel
'staff' => \App\Http\Middleware\Staff::class,
'log' => \App\Http\Middleware\RequestLog::class,
'server' => \App\Http\Middleware\Server::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
/**

View File

@@ -3,9 +3,8 @@
namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Services\AuthService;
use Illuminate\Support\Facades\Auth;
use Closure;
use Illuminate\Support\Facades\Cache;
class Admin
{
@@ -18,14 +17,15 @@ class Admin
*/
public function handle($request, Closure $next)
{
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) throw new ApiException('未登录或登陆已过期', 403);
if (!Auth::guard('sanctum')->check()) {
throw new ApiException('未登录或登陆已过期', 403);
}
$user = Auth::guard('sanctum')->user();
if (!$user->is_admin) {
throw new ApiException('无管理员权限', 403);
}
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_admin']) throw new ApiException('未登录或登陆已过期',403);
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : null;
}
}

View File

@@ -3,10 +3,9 @@
namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Utils\CacheKey;
use Closure;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Auth;
class Client
{
@@ -19,7 +18,7 @@ class Client
*/
public function handle($request, Closure $next)
{
$token = $request->input('token');
$token = $request->input('token', $request->route('token'));
if (empty($token)) {
throw new ApiException('token is null',403);
}
@@ -27,9 +26,8 @@ class Client
if (!$user) {
throw new ApiException('token is error',403);
}
$request->merge([
'user' => $user
]);
Auth::setUser($user);
return $next($request);
}
}

View File

@@ -1,54 +1,55 @@
<?php
namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Models\Server as ServerModel;
use App\Services\ServerService;
use Closure;
use Illuminate\Http\Request;
class Server
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, $node_type = null)
public function handle(Request $request, Closure $next, ?string $nodeType = null)
{
$this->validateRequest($request);
$serverInfo = ServerService::getServer(
$request->input('node_id'),
$request->input('node_type') ?? $nodeType
);
if (!$serverInfo) {
throw new ApiException('Server does not exist');
}
$request->merge(['node_info' => $serverInfo]);
return $next($request);
}
private function validateRequest(Request $request): void
{
// alias
$aliasTypes = [
'v2ray' => 'vmess',
'hysteria2' => 'hysteria'
];
$request->validate([
'token' => [
"string",
"required",
'string',
'required',
function ($attribute, $value, $fail) {
if ($value !== admin_setting('server_token')) {
$fail('The ' . $attribute . ' is invalid.');
$fail("Invalid {$attribute}");
}
},
],
'node_id' => 'required',
'node_type' => [
'required',
'nullable',
'regex:/^(?i)(hysteria|hysteria2|vless|trojan|vmess|v2ray|tuic|shadowsocks|shadowsocks-plugin)$/',
function ($attribute, $value, $fail) use ($aliasTypes, $request) {
$request->merge([$attribute => strtolower(isset($aliasTypes[$value]) ? $aliasTypes[$value] : $value)]);
function ($attribute, $value, $fail) use ($request) {
if (!ServerModel::isValidType($value)) {
$fail("Invalid node type specified");
return;
}
$request->merge([$attribute => ServerModel::normalizeType($value)]);
},
]
], [
'node_type.regex' => 'node_type is error!'
]);
$nodeInfo = ServerService::getServer($request->input('node_id'), $request->input('node_type') ?? $node_type);
if (!$nodeInfo)
throw new ApiException('server is not exist!');
$request->merge(['node_info' => $nodeInfo]);
return $next($request);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Middleware;
use App\Exceptions\ApiException;
use App\Services\AuthService;
use Auth;
use Closure;
use Illuminate\Support\Facades\Cache;
@@ -18,14 +19,9 @@ class User
*/
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) throw new ApiException('未登录或登陆已过期', 403);
$request->merge([
'user' => $user
]);
if (!Auth::guard('sanctum')->check()) {
throw new ApiException('未登录或登陆已过期', 403);
}
return $next($request);
}
}

View File

@@ -8,46 +8,48 @@ class ConfigSave extends FormRequest
{
const RULES = [
// invite & commission
'invite_force' => 'in:0,1',
'invite_commission' => 'integer',
'invite_gen_limit' => 'integer',
'invite_never_expire' => 'in:0,1',
'commission_first_time_enable' => 'in:0,1',
'commission_auto_check_enable' => 'in:0,1',
'invite_force' => '',
'invite_commission' => 'integer|nullable',
'invite_gen_limit' => 'integer|nullable',
'invite_never_expire' => '',
'commission_first_time_enable' => '',
'commission_auto_check_enable' => '',
'commission_withdraw_limit' => 'nullable|numeric',
'commission_withdraw_method' => 'nullable|array',
'withdraw_close_enable' => 'in:0,1',
'commission_distribution_enable' => 'in:0,1',
'withdraw_close_enable' => '',
'commission_distribution_enable' => '',
'commission_distribution_l1' => 'nullable|numeric',
'commission_distribution_l2' => 'nullable|numeric',
'commission_distribution_l3' => 'nullable|numeric',
// site
'logo' => 'nullable|url',
'force_https' => 'in:0,1',
'stop_register' => 'in:0,1',
'force_https' => '',
'stop_register' => '',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable',
'try_out_enable' => 'in:0,1',
'try_out_enable' => '',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'tos_url' => 'nullable|url',
'currency' => '',
'currency_symbol' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'plan_change_enable' => '',
'reset_traffic_method' => 'in:0,1,2,3,4',
'surplus_enable' => 'in:0,1',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
'change_order_event_id' => 'in:0,1',
'show_info_to_server_enable' => 'in:0,1',
'show_protocol_to_server_enable' => 'in:0,1',
'surplus_enable' => '',
'new_order_event_id' => '',
'renew_order_event_id' => '',
'change_order_event_id' => '',
'show_info_to_server_enable' => '',
'show_protocol_to_server_enable' => '',
'subscribe_path' => '',
// server
'server_token' => 'nullable|min:16',
'server_pull_interval' => 'integer',
'server_push_interval' => 'integer',
'device_limit_mode' => 'integer',
// frontend
'frontend_theme' => '',
'frontend_theme_sidebar' => 'nullable|in:dark,light',
@@ -62,8 +64,9 @@ class ConfigSave extends FormRequest
'email_password' => '',
'email_encryption' => '',
'email_from_address' => '',
'remind_mail_enable' => '',
// telegram
'telegram_bot_enable' => 'in:0,1',
'telegram_bot_enable' => '',
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_id' => '',
@@ -76,23 +79,23 @@ class ConfigSave extends FormRequest
'android_version' => '',
'android_download_url' => '',
// safe
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_enable' => 'boolean',
'email_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'email_gmail_limit_enable' => 'boolean',
'recaptcha_enable' => 'boolean',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'email_verify' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'register_limit_by_ip_enable' => 'in:0,1',
'email_verify' => 'bool',
'safe_mode_enable' => 'boolean',
'register_limit_by_ip_enable' => 'boolean',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
'secure_path' => 'min:8|regex:/^[\w-]*$/',
'password_limit_enable' => 'in:0,1',
'password_limit_enable' => 'boolean',
'password_limit_count' => 'integer',
'password_limit_expire' => 'integer',
'default_remind_expire' => 'integer',
'default_remind_traffic' => 'integer'
'default_remind_expire' => 'boolean',
'default_remind_traffic' => 'boolean'
];
/**
* Get the validation rules that apply to the request.

View File

@@ -0,0 +1,115 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\Server;
use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends FormRequest
{
private const PROTOCOL_RULES = [
'shadowsocks' => [
'cipher' => 'required|string',
'obfs' => 'nullable|string',
'obfs_settings.path' => 'nullable|string',
'obfs_settings.host' => 'nullable|string',
],
'vmess' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
],
'trojan' => [
'network' => 'required|string',
'network_settings' => 'nullable|array',
'server_name' => 'nullable|string',
'allow_insecure' => 'nullable|boolean',
],
'hysteria' => [
'version' => 'required|integer',
'alpn' => 'nullable|string',
'obfs.open' => 'nullable|boolean',
'obfs.type' => 'string|nullable',
'obfs.password' => 'string|nullable',
'tls.server_name' => 'nullable|string',
'tls.allow_insecure' => 'nullable|boolean',
'bandwidth.up' => 'nullable|integer',
'bandwidth.down' => 'nullable|integer',
],
'vless' => [
'tls' => 'required|integer',
'network' => 'required|string',
'network_settings' => 'nullable|array',
'flow' => 'nullable|string',
'tls_settings.server_name' => 'nullable|string',
'tls_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.allow_insecure' => 'nullable|boolean',
'reality_settings.server_name' => 'nullable|string',
'reality_settings.server_port' => 'nullable|integer',
'reality_settings.public_key' => 'nullable|string',
'reality_settings.private_key' => 'nullable|string',
'reality_settings.short_id' => 'nullable|string',
]
];
private function getBaseRules(): array
{
return [
'type' => 'required|in:' . implode(',', Server::VALID_TYPES),
'spectific_key' => 'nullable|string',
'code' => 'nullable|string',
'show' => '',
'name' => 'required|string',
'group_ids' => 'nullable|array',
'route_ids' => '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',
'protocol_settings' => 'array',
];
}
public function rules(): array
{
$type = $this->input('type');
$rules = $this->getBaseRules();
foreach (self::PROTOCOL_RULES[$type] ?? [] as $field => $rule) {
$rules['protocol_settings.' . $field] = $rule;
}
return $rules;
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_ids.required' => '权限组不能为空',
'group_ids.array' => '权限组格式不正确',
'route_ids.array' => '路由组格式不正确',
'parent_id.integer' => '父ID格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'tls.required' => 'TLS不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'network.required' => '传输协议不能为空',
'network.in' => '传输协议格式不正确',
'networkSettings.array' => '传输协议配置有误',
'ruleSettings.array' => '规则配置有误',
'tlsSettings.array' => 'tls配置有误',
'dnsSettings.array' => 'dns配置有误'
];
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerShadowsocksSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'parent_id' => 'nullable|integer',
'route_id' => 'nullable|array',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'cipher' => 'required|in:aes-128-gcm,aes-192-gcm,aes-256-gcm,chacha20-ietf-poly1305,2022-blake3-aes-128-gcm,2022-blake3-aes-256-gcm',
'obfs' => 'nullable|in:http',
'obfs_settings' => 'nullable|array',
'tags' => 'nullable|array',
'excludes' => 'nullable|array',
'ips' => 'nullable|array',
'rate' => 'required|numeric'
];
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'route_id.array' => '路由组格式不正确',
'parent_id.integer' => '父节点格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'cipher.required' => '加密方式不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'obfs.in' => '混淆格式不正确',
'obfs_settings.array' => '混淆设置格式不正确'
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ class UserUpdate extends FormRequest
'password' => 'nullable|min:8',
'transfer_enable' => 'numeric',
'expired_at' => 'nullable|integer',
'banned' => 'required|in:0,1',
'banned' => 'in:0,1',
'plan_id' => 'nullable|integer',
'commission_rate' => 'nullable|integer|min:0|max:100',
'discount' => 'nullable|integer|min:0|max:100',
@@ -26,11 +26,12 @@ class UserUpdate extends FormRequest
'is_staff' => 'required|in:0,1',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'balance' => 'numeric',
'commission_type' => 'integer',
'commission_balance' => 'integer',
'commission_balance' => 'numeric',
'remarks' => 'nullable',
'speed_limit' => 'nullable|integer'
'speed_limit' => 'nullable|integer',
'device_limit' => 'nullable|integer'
];
}
@@ -41,7 +42,6 @@ class UserUpdate extends FormRequest
'email.email' => '邮箱格式不正确',
'transfer_enable.numeric' => '流量格式不正确',
'expired_at.integer' => '到期时间格式不正确',
'banned.required' => '是否封禁不能为空',
'banned.in' => '是否封禁格式不正确',
'is_admin.required' => '是否管理员不能为空',
'is_admin.in' => '是否管理员格式不正确',
@@ -61,7 +61,8 @@ class UserUpdate extends FormRequest
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确',
'password.min' => '密码长度最小8位',
'speed_limit.integer' => '限速格式不正确'
'speed_limit.integer' => '限速格式不正确',
'device_limit.integer' => '设备数量格式不正确'
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
...parent::toArray($request),
'period' => PlanService::getLegacyPeriod($this->period),
'plan' => PlanResource::make($this->plan),
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Http\Resources;
use App\Models\Plan;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PlanResource extends JsonResource
{
private const PRICE_MULTIPLIER = 100;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->resource['id'],
'group_id' => $this->resource['group_id'],
'name' => $this->resource['name'],
'content' => $this->resource['content'],
...$this->getPeriodPrices(),
'capacity_limit' => $this->getFormattedCapacityLimit(),
'transfer_enable' => $this->resource['transfer_enable'],
'speed_limit' => $this->resource['speed_limit'],
'show' => (bool) $this->resource['show'],
'sell' => (bool) $this->resource['sell'],
'renew' => (bool) $this->resource['renew'],
'reset_traffic_method' => $this->resource['reset_traffic_method'],
'sort' => $this->resource['sort'],
'created_at' => $this->resource['created_at'],
'updated_at' => $this->resource['updated_at']
];
}
/**
* Get transformed period prices using Plan mapping
*
* @return array<string, float|null>
*/
protected function getPeriodPrices(): array
{
return collect(Plan::LEGACY_PERIOD_MAPPING)
->mapWithKeys(function (string $newPeriod, string $legacyPeriod): array {
$price = $this->resource['prices'][$newPeriod] ?? null;
return [
$legacyPeriod => $price !== null
? (float) $price * self::PRICE_MULTIPLIER
: null
];
})
->all();
}
/**
* Get formatted capacity limit value
*
* @return int|string|null
*/
protected function getFormattedCapacityLimit(): int|string|null
{
$limit = $this->resource['capacity_limit'];
return match (true) {
$limit === null => null,
$limit <= 0 => __('Sold out'),
default => (int) $limit,
};
}
}

View File

@@ -1,144 +0,0 @@
<?php
namespace App\Http\Routes\V1;
use Illuminate\Contracts\Routing\Registrar;
class AdminRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
'middleware' => ['admin', 'log'],
], function ($router) {
// Config
$router->get ('/config/fetch', 'V1\\Admin\\ConfigController@fetch');
$router->post('/config/save', 'V1\\Admin\\ConfigController@save');
$router->get ('/config/getEmailTemplate', 'V1\\Admin\\ConfigController@getEmailTemplate');
$router->get ('/config/getThemeTemplate', 'V1\\Admin\\ConfigController@getThemeTemplate');
$router->post('/config/setTelegramWebhook', 'V1\\Admin\\ConfigController@setTelegramWebhook');
$router->post('/config/testSendMail', 'V1\\Admin\\ConfigController@testSendMail');
// Plan
$router->get ('/plan/fetch', 'V1\\Admin\\PlanController@fetch');
$router->post('/plan/save', 'V1\\Admin\\PlanController@save');
$router->post('/plan/drop', 'V1\\Admin\\PlanController@drop');
$router->post('/plan/update', 'V1\\Admin\\PlanController@update');
$router->post('/plan/sort', 'V1\\Admin\\PlanController@sort');
// Server
$router->get ('/server/group/fetch', 'V1\\Admin\\Server\\GroupController@fetch');
$router->post('/server/group/save', 'V1\\Admin\\Server\\GroupController@save');
$router->post('/server/group/drop', 'V1\\Admin\\Server\\GroupController@drop');
$router->get ('/server/route/fetch', 'V1\\Admin\\Server\\RouteController@fetch');
$router->post('/server/route/save', 'V1\\Admin\\Server\\RouteController@save');
$router->post('/server/route/drop', 'V1\\Admin\\Server\\RouteController@drop');
$router->get ('/server/manage/getNodes', 'V1\\Admin\\Server\\ManageController@getNodes');
$router->post('/server/manage/sort', 'V1\\Admin\\Server\\ManageController@sort');
$router->group([
'prefix' => 'server/trojan'
], function ($router) {
$router->post('save', 'V1\\Admin\\Server\\TrojanController@save');
$router->post('drop', 'V1\\Admin\\Server\\TrojanController@drop');
$router->post('update', 'V1\\Admin\\Server\\TrojanController@update');
$router->post('copy', 'V1\\Admin\\Server\\TrojanController@copy');
});
$router->group([
'prefix' => 'server/vmess'
], function ($router) {
$router->post('save', 'V1\\Admin\\Server\\VmessController@save');
$router->post('drop', 'V1\\Admin\\Server\\VmessController@drop');
$router->post('update', 'V1\\Admin\\Server\\VmessController@update');
$router->post('copy', 'V1\\Admin\\Server\\VmessController@copy');
});
$router->group([
'prefix' => 'server/shadowsocks'
], function ($router) {
$router->post('save', 'V1\\Admin\\Server\\ShadowsocksController@save');
$router->post('drop', 'V1\\Admin\\Server\\ShadowsocksController@drop');
$router->post('update', 'V1\\Admin\\Server\\ShadowsocksController@update');
$router->post('copy', 'V1\\Admin\\Server\\ShadowsocksController@copy');
});
$router->group([
'prefix' => 'server/hysteria'
], function ($router) {
$router->post('save', 'V1\\Admin\\Server\\HysteriaController@save');
$router->post('drop', 'V1\\Admin\\Server\\HysteriaController@drop');
$router->post('update', 'V1\\Admin\\Server\\HysteriaController@update');
$router->post('copy', 'V1\\Admin\\Server\\HysteriaController@copy');
});
$router->group([
'prefix' => 'server/vless'
], function ($router) {
$router->post('save', 'V1\\Admin\\Server\\VlessController@save');
$router->post('drop', 'V1\\Admin\\Server\\VlessController@drop');
$router->post('update', 'V1\\Admin\\Server\\VlessController@update');
$router->post('copy', 'V1\\Admin\\Server\\VlessController@copy');
});
// Order
$router->get ('/order/fetch', 'V1\\Admin\\OrderController@fetch');
$router->post('/order/update', 'V1\\Admin\\OrderController@update');
$router->post('/order/assign', 'V1\\Admin\\OrderController@assign');
$router->post('/order/paid', 'V1\\Admin\\OrderController@paid');
$router->post('/order/cancel', 'V1\\Admin\\OrderController@cancel');
$router->post('/order/detail', 'V1\\Admin\\OrderController@detail');
// User
$router->get ('/user/fetch', 'V1\\Admin\\UserController@fetch');
$router->post('/user/update', 'V1\\Admin\\UserController@update');
$router->get ('/user/getUserInfoById', 'V1\\Admin\\UserController@getUserInfoById');
$router->post('/user/generate', 'V1\\Admin\\UserController@generate');
$router->post('/user/dumpCSV', 'V1\\Admin\\UserController@dumpCSV');
$router->post('/user/sendMail', 'V1\\Admin\\UserController@sendMail');
$router->post('/user/ban', 'V1\\Admin\\UserController@ban');
$router->post('/user/resetSecret', 'V1\\Admin\\UserController@resetSecret');
$router->post('/user/setInviteUser', 'V1\\Admin\\UserController@setInviteUser');
// Stat
$router->get ('/stat/getStat', 'V1\\Admin\\StatController@getStat');
$router->get ('/stat/getOverride', 'V1\\Admin\\StatController@getOverride');
$router->get ('/stat/getServerLastRank', 'V1\\Admin\\StatController@getServerLastRank');
$router->get ('/stat/getServerYesterdayRank', 'V1\\Admin\\StatController@getServerYesterdayRank');
$router->get ('/stat/getOrder', 'V1\\Admin\\StatController@getOrder');
$router->get ('/stat/getStatUser', 'V1\\Admin\\StatController@getStatUser');
$router->get ('/stat/getRanking', 'V1\\Admin\\StatController@getRanking');
$router->get ('/stat/getStatRecord', 'V1\\Admin\\StatController@getStatRecord');
// Notice
$router->get ('/notice/fetch', 'V1\\Admin\\NoticeController@fetch');
$router->post('/notice/save', 'V1\\Admin\\NoticeController@save');
$router->post('/notice/update', 'V1\\Admin\\NoticeController@update');
$router->post('/notice/drop', 'V1\\Admin\\NoticeController@drop');
$router->post('/notice/show', 'V1\\Admin\\NoticeController@show');
// Ticket
$router->get ('/ticket/fetch', 'V1\\Admin\\TicketController@fetch');
$router->post('/ticket/reply', 'V1\\Admin\\TicketController@reply');
$router->post('/ticket/close', 'V1\\Admin\\TicketController@close');
// Coupon
$router->get ('/coupon/fetch', 'V1\\Admin\\CouponController@fetch');
$router->post('/coupon/generate', 'V1\\Admin\\CouponController@generate');
$router->post('/coupon/drop', 'V1\\Admin\\CouponController@drop');
$router->post('/coupon/show', 'V1\\Admin\\CouponController@show');
// Knowledge
$router->get ('/knowledge/fetch', 'V1\\Admin\\KnowledgeController@fetch');
$router->get ('/knowledge/getCategory', 'V1\\Admin\\KnowledgeController@getCategory');
$router->post('/knowledge/save', 'V1\\Admin\\KnowledgeController@save');
$router->post('/knowledge/show', 'V1\\Admin\\KnowledgeController@show');
$router->post('/knowledge/drop', 'V1\\Admin\\KnowledgeController@drop');
$router->post('/knowledge/sort', 'V1\\Admin\\KnowledgeController@sort');
// Payment
$router->get ('/payment/fetch', 'V1\\Admin\\PaymentController@fetch');
$router->get ('/payment/getPaymentMethods', 'V1\\Admin\\PaymentController@getPaymentMethods');
$router->post('/payment/getPaymentForm', 'V1\\Admin\\PaymentController@getPaymentForm');
$router->post('/payment/save', 'V1\\Admin\\PaymentController@save');
$router->post('/payment/drop', 'V1\\Admin\\PaymentController@drop');
$router->post('/payment/show', 'V1\\Admin\\PaymentController@show');
$router->post('/payment/sort', 'V1\\Admin\\PaymentController@sort');
// System
$router->get ('/system/getSystemStatus', 'V1\\Admin\\SystemController@getSystemStatus');
$router->get ('/system/getQueueStats', 'V1\\Admin\\SystemController@getQueueStats');
$router->get ('/system/getQueueWorkload', 'V1\\Admin\\SystemController@getQueueWorkload');
$router->get ('/system/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
$router->get ('/system/getSystemLog', 'V1\\Admin\\SystemController@getSystemLog');
// Theme
$router->get ('/theme/getThemes', 'V1\\Admin\\ThemeController@getThemes');
$router->post('/theme/saveThemeConfig', 'V1\\Admin\\ThemeController@saveThemeConfig');
$router->post('/theme/getThemeConfig', 'V1\\Admin\\ThemeController@getThemeConfig');
});
}
}

View File

@@ -12,7 +12,7 @@ class ClientRoute
'middleware' => 'client'
], function ($router) {
// Client
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe');
$router->get('/subscribe', 'V1\\Client\\ClientController@subscribe')->name('client.subscribe.legacy');
// App
$router->get('/app/getConfig', 'V1\\Client\\AppController@getConfig');
$router->get('/app/getVersion', 'V1\\Client\\AppController@getVersion');

View File

@@ -1,6 +1,10 @@
<?php
namespace App\Http\Routes\V1;
use App\Http\Controllers\V1\Guest\CommController;
use App\Http\Controllers\V1\Guest\PaymentController;
use App\Http\Controllers\V1\Guest\PlanController;
use App\Http\Controllers\V1\Guest\TelegramController;
use Illuminate\Contracts\Routing\Registrar;
class GuestRoute
@@ -11,13 +15,13 @@ class GuestRoute
'prefix' => 'guest'
], function ($router) {
// Plan
$router->get ('/plan/fetch', 'V1\\Guest\\PlanController@fetch');
$router->get('/plan/fetch', [PlanController::class, 'fetch']);
// Telegram
$router->post('/telegram/webhook', 'V1\\Guest\\TelegramController@webhook');
$router->post('/telegram/webhook', [TelegramController::class, 'webhook']);
// Payment
$router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', 'V1\\Guest\\PaymentController@notify');
$router->match(['get', 'post'], '/payment/notify/{method}/{uuid}', [PaymentController::class, 'notify']);
// Comm
$router->get ('/comm/config', 'V1\\Guest\\CommController@config');
$router->get('/comm/config', [CommController::class, 'config']);
});
}
}

View File

@@ -17,19 +17,12 @@ class ServerRoute
$router->group([
'prefix' => 'UniProxy',
'middleware' => 'server'
] ,function ($route) {
], function ($route) {
$route->get('config', [UniProxyController::class, 'config']);
$route->get('user', [UniProxyController::class, 'user']);
$route->post('push', [UniProxyController::class, 'push']);
$route->post('alive', [UniProxyController::class, 'alive']);
});
$router->group([
'prefix' => 'Deepbwork',
'middleware' => 'server:vmess'
], function ($route) {
$route->get('config', [DeepbworkController::class, 'config']);
$route->get('user', [DeepbworkController::class, 'user']);
$route->post('submit', [DeepbworkController::class, 'submit']);
$route->get('alivelist', [UniProxyController::class, 'alivelist']);
});
$router->group([
'prefix' => 'ShadowsocksTidalab',

View File

@@ -12,21 +12,21 @@ class StaffRoute
'middleware' => 'staff'
], function ($router) {
// Ticket
$router->get ('/ticket/fetch', 'V1\\Staff\\TicketController@fetch');
$router->post('/ticket/reply', 'V1\\Staff\\TicketController@reply');
$router->post('/ticket/close', 'V1\\Staff\\TicketController@close');
// User
$router->post('/user/update', 'V1\\Staff\\UserController@update');
$router->get ('/user/getUserInfoById', 'V1\\Staff\\UserController@getUserInfoById');
$router->post('/user/sendMail', 'V1\\Staff\\UserController@sendMail');
$router->post('/user/ban', 'V1\\Staff\\UserController@ban');
// Plan
$router->get ('/plan/fetch', 'V1\\Staff\\PlanController@fetch');
// Notice
$router->get ('/notice/fetch', 'V1\\Admin\\NoticeController@fetch');
$router->post('/notice/save', 'V1\\Admin\\NoticeController@save');
$router->post('/notice/update', 'V1\\Admin\\NoticeController@update');
$router->post('/notice/drop', 'V1\\Admin\\NoticeController@drop');
// $router->get ('/ticket/fetch', 'V1\\Staff\\TicketController@fetch');
// $router->post('/ticket/reply', 'V1\\Staff\\TicketController@reply');
// $router->post('/ticket/close', 'V1\\Staff\\TicketController@close');
// // User
// $router->post('/user/update', 'V1\\Staff\\UserController@update');
// $router->get ('/user/getUserInfoById', 'V1\\Staff\\UserController@getUserInfoById');
// $router->post('/user/sendMail', 'V1\\Staff\\UserController@sendMail');
// $router->post('/user/ban', 'V1\\Staff\\UserController@ban');
// // Plan
// $router->get ('/plan/fetch', 'V1\\Staff\\PlanController@fetch');
// // Notice
// $router->get ('/notice/fetch', 'V1\\Admin\\NoticeController@fetch');
// $router->post('/notice/save', 'V1\\Admin\\NoticeController@save');
// $router->post('/notice/update', 'V1\\Admin\\NoticeController@update');
// $router->post('/notice/drop', 'V1\\Admin\\NoticeController@drop');
});
}
}

View File

@@ -1,7 +1,23 @@
<?php
namespace App\Http\Routes\V2;
use App\Http\Controllers\V2\Admin\ConfigController;
use App\Http\Controllers\V2\Admin\PlanController;
use App\Http\Controllers\V2\Admin\Server\GroupController;
use App\Http\Controllers\V2\Admin\Server\RouteController;
use App\Http\Controllers\V2\Admin\Server\ManageController;
use App\Http\Controllers\V2\Admin\OrderController;
use App\Http\Controllers\V2\Admin\UserController;
use App\Http\Controllers\V2\Admin\StatController;
use App\Http\Controllers\V2\Admin\NoticeController;
use App\Http\Controllers\V2\Admin\TicketController;
use App\Http\Controllers\V2\Admin\CouponController;
use App\Http\Controllers\V2\Admin\KnowledgeController;
use App\Http\Controllers\V2\Admin\PaymentController;
use App\Http\Controllers\V2\Admin\SystemController;
use App\Http\Controllers\V2\Admin\ThemeController;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Support\Facades\Route;
class AdminRoute
{
@@ -11,10 +27,196 @@ class AdminRoute
'prefix' => admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))),
'middleware' => ['admin', 'log'],
], function ($router) {
// Config
$router->group([
'prefix' => 'config'
], function ($router) {
$router->get('/fetch', [ConfigController::class, 'fetch']);
$router->post('/save', [ConfigController::class, 'save']);
$router->get('/getEmailTemplate', [ConfigController::class, 'getEmailTemplate']);
$router->get('/getThemeTemplate', [ConfigController::class, 'getThemeTemplate']);
$router->post('/setTelegramWebhook', [ConfigController::class, 'setTelegramWebhook']);
$router->post('/testSendMail', [ConfigController::class, 'testSendMail']);
});
// Plan
$router->group([
'prefix' => 'plan'
], function ($router) {
$router->get('/fetch', [PlanController::class, 'fetch']);
$router->post('/save', [PlanController::class, 'save']);
$router->post('/drop', [PlanController::class, 'drop']);
$router->post('/update', [PlanController::class, 'update']);
$router->post('/sort', [PlanController::class, 'sort']);
});
// Server
$router->group([
'prefix' => 'server/group'
], function ($router) {
$router->get('/fetch', [GroupController::class, 'fetch']);
$router->post('/save', [GroupController::class, 'save']);
$router->post('/drop', [GroupController::class, 'drop']);
});
$router->group([
'prefix' => 'server/route'
], function ($router) {
$router->get('/fetch', [RouteController::class, 'fetch']);
$router->post('/save', [RouteController::class, 'save']);
$router->post('/drop', [RouteController::class, 'drop']);
});
$router->group([
'prefix' => 'server/manage'
], function ($router) {
$router->get('/getNodes', [ManageController::class, 'getNodes']);
$router->post('/sort', [ManageController::class, 'sort']);
});
// 节点更新接口
$router->group([
'prefix' => 'server/manage'
], function ($router) {
$router->post('/update', [ManageController::class, 'update']);
$router->post('/save', [ManageController::class, 'save']);
$router->post('/drop', [ManageController::class, 'drop']);
$router->post('/copy', [ManageController::class, 'copy']);
$router->post('/sort', [ManageController::class, 'sort']);
});
// Order
$router->group([
'prefix' => 'order'
], function ($router) {
$router->any('/fetch', [OrderController::class, 'fetch']);
$router->post('/update', [OrderController::class, 'update']);
$router->post('/assign', [OrderController::class, 'assign']);
$router->post('/paid', [OrderController::class, 'paid']);
$router->post('/cancel', [OrderController::class, 'cancel']);
$router->post('/detail', [OrderController::class, 'detail']);
});
// User
$router->group([
'prefix' => 'user'
], function ($router) {
$router->any('/fetch', [UserController::class, 'fetch']);
$router->post('/update', [UserController::class, 'update']);
$router->get('/getUserInfoById', [UserController::class, 'getUserInfoById']);
$router->post('/generate', [UserController::class, 'generate']);
$router->post('/dumpCSV', [UserController::class, 'dumpCSV']);
$router->post('/user/sendMail', [UserController::class, 'sendMail']);
$router->post('/ban', [UserController::class, 'ban']);
$router->post('/resetSecret', [UserController::class, 'resetSecret']);
$router->post('/setInviteUser', [UserController::class, 'setInviteUser']);
});
// Stat
$router->get ('/stat/override', 'V2\\Admin\\StatController@override');
$router->get ('/stat/record', 'V2\\Admin\\StatController@record');
$router->get ('/stat/ranking', 'V2\\Admin\\StatController@ranking');
$router->group([
'prefix' => 'stat'
], function ($router) {
$router->get('/getOverride', [StatController::class, 'getOverride']);
$router->get('/getStats', [StatController::class, 'getStats']);
$router->get('/getServerLastRank', [StatController::class, 'getServerLastRank']);
$router->get('/getServerYesterdayRank', [StatController::class, 'getServerYesterdayRank']);
$router->get('/getOrder', [StatController::class, 'getOrder']);
$router->any('/getStatUser', [StatController::class, 'getStatUser']);
$router->get('/getRanking', [StatController::class, 'getRanking']);
$router->get('/getStatRecord', [StatController::class, 'getStatRecord']);
$router->get('/getTrafficRank', [StatController::class, 'getTrafficRank']);
});
// Notice
$router->group([
'prefix' => 'notice'
], function ($router) {
$router->get('/fetch', [NoticeController::class, 'fetch']);
$router->post('/save', [NoticeController::class, 'save']);
$router->post('/update', [NoticeController::class, 'update']);
$router->post('/drop', [NoticeController::class, 'drop']);
$router->post('/show', [NoticeController::class, 'show']);
$router->post('/sort', [NoticeController::class, 'sort']);
});
// Ticket
$router->group([
'prefix' => 'ticket'
], function ($router) {
$router->any('/fetch', [TicketController::class, 'fetch']);
$router->post('/reply', [TicketController::class, 'reply']);
$router->post('/close', [TicketController::class, 'close']);
});
// Coupon
$router->group([
'prefix' => 'coupon'
], function ($router) {
$router->any('/fetch', [CouponController::class, 'fetch']);
$router->post('/generate', [CouponController::class, 'generate']);
$router->post('/drop', [CouponController::class, 'drop']);
$router->post('/show', [CouponController::class, 'show']);
$router->post('/update', [CouponController::class, 'update']);
});
// Knowledge
$router->group([
'prefix' => 'knowledge'
], function ($router) {
$router->get('/fetch', [KnowledgeController::class, 'fetch']);
$router->get('/getCategory', [KnowledgeController::class, 'getCategory']);
$router->post('/save', [KnowledgeController::class, 'save']);
$router->post('/show', [KnowledgeController::class, 'show']);
$router->post('/drop', [KnowledgeController::class, 'drop']);
$router->post('/sort', [KnowledgeController::class, 'sort']);
});
// Payment
$router->group([
'prefix' => 'payment'
], function ($router) {
$router->get('/fetch', [PaymentController::class, 'fetch']);
$router->get('/getPaymentMethods', [PaymentController::class, 'getPaymentMethods']);
$router->post('/getPaymentForm', [PaymentController::class, 'getPaymentForm']);
$router->post('/save', [PaymentController::class, 'save']);
$router->post('/drop', [PaymentController::class, 'drop']);
$router->post('/show', [PaymentController::class, 'show']);
$router->post('/sort', [PaymentController::class, 'sort']);
});
// System
$router->group([
'prefix' => 'system'
], function ($router) {
$router->get('/getSystemStatus', [SystemController::class, 'getSystemStatus']);
$router->get('/getQueueStats', [SystemController::class, 'getQueueStats']);
$router->get('/getQueueWorkload', [SystemController::class, 'getQueueWorkload']);
$router->get('/getQueueMasters', '\\Laravel\\Horizon\\Http\\Controllers\\MasterSupervisorController@index');
$router->get('/getSystemLog', [SystemController::class, 'getSystemLog']);
});
// Theme
$router->group([
'prefix' => 'theme'
], function ($router) {
$router->get('/getThemes', [ThemeController::class, 'getThemes']);
$router->post('/upload', [ThemeController::class, 'upload']);
$router->post('/delete', [ThemeController::class, 'delete']);
$router->post('/saveThemeConfig', [ThemeController::class, 'saveThemeConfig']);
$router->post('/getThemeConfig', [ThemeController::class, 'getThemeConfig']);
});
// Plugin
$router->group([
'prefix' => 'plugin'
], function ($router) {
$router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']);
$router->post('install', [\App\Http\Controllers\V2\Admin\PluginController::class, 'install']);
$router->post('uninstall', [\App\Http\Controllers\V2\Admin\PluginController::class, 'uninstall']);
$router->post('enable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'enable']);
$router->post('disable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'disable']);
$router->get('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'getConfig']);
$router->post('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'updateConfig']);
});
});
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Routes\V2;
use Illuminate\Contracts\Routing\Registrar;
class PassportRoute
{
public function map(Registrar $router)
{
$router->group([
'prefix' => 'passport'
], function ($router) {
// Auth
$router->post('/auth/register', 'V1\\Passport\\AuthController@register');
$router->post('/auth/login', 'V1\\Passport\\AuthController@login');
$router->get ('/auth/token2Login', 'V1\\Passport\\AuthController@token2Login');
$router->post('/auth/forget', 'V1\\Passport\\AuthController@forget');
$router->post('/auth/getQuickLoginUrl', 'V1\\Passport\\AuthController@getQuickLoginUrl');
$router->post('/auth/loginWithMailLink', 'V1\\Passport\\AuthController@loginWithMailLink');
// Comm
$router->post('/comm/sendEmailVerify', 'V1\\Passport\\CommController@sendEmailVerify');
$router->post('/comm/pv', 'V1\\Passport\\CommController@pv');
});
}
}

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