增加 流量充值

This commit is contained in:
iVampireSP.com 2023-05-16 20:41:33 +08:00
parent 8bc35a78ea
commit c5eff6ad4e
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
10 changed files with 510 additions and 23 deletions

View File

@ -3,10 +3,94 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Support\WHMCS;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Http\Request;
class TrafficController extends Controller class TrafficController extends Controller
{ {
public function price()
{
return $this->success([
'price_per_gb' => config('settings.price_per_gb')
]);
}
public function providers()
{
$config = config('whmcs');
// 获取 config 的所有的 key
$providers = array_keys($config);
return $this->success($providers);
}
public function payments(Request $request, $provider)
{
try {
$whmcs = new WHMCS($provider);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
$payments = $whmcs->getPayments();
return $this->success($payments);
}
public function index(Request $request)
{
$user = auth()->user();
$traffic = $user->traffic ?? 0;
$day = now()->day;
$last_sign_at = Cache::get('traffic_sign:' . $day . '-' . $user->id, null);
return $this->success([
'traffic' => $traffic,
'is_signed' => $last_sign_at
]);
}
public function charge(Request $request, string $provider)
{
$request->validate([
'payment' => 'required',
'traffic' => 'required|integer|min:1'
]);
$price = bcmul(config('settings.price_per_gb'), $request->input('traffic'), 2);
try {
$whmcs = new WHMCS($provider);
} catch (\Exception $e) {
return $this->error('提供商不存在');
}
if (!$whmcs->hasPayment($request->input('payment'))) {
return $this->notFound('支付方式不存在');
}
$user = $request->user();
try {
$result = $whmcs->api_addTraffic($user->email, $request->input('payment'), $request->input('traffic'), $price);
return $this->success($result);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
public function free() public function free()
{ {
$user = auth()->user(); $user = auth()->user();

View File

@ -4,29 +4,30 @@
use App\Http\Middleware\Admin; use App\Http\Middleware\Admin;
use App\Http\Middleware\ApiToken; use App\Http\Middleware\ApiToken;
use App\Http\Middleware\Authenticate; use App\Http\Middleware\WHMCSApi;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\TrimStrings; use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\TrustProxies; use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature; use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\VerifyCsrfToken; use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified; use App\Http\Middleware\ValidateSignature;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\HandleCors; use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Http\Middleware\SetCacheHeaders; use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use App\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
class Kernel extends HttpKernel class Kernel extends HttpKernel
@ -89,5 +90,6 @@ class Kernel extends HttpKernel
'throttle' => ThrottleRequests::class, 'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class, 'verified' => EnsureEmailIsVerified::class,
'api_token' => ApiToken::class, 'api_token' => ApiToken::class,
'whmcs_api' => WHMCSApi::class,
]; ];
} }

View File

@ -0,0 +1,76 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
class WHMCSApi
{
public function handle(Request $request, Closure $next)
{
// add json header
$request->headers->set('Accept', 'application/json');
// bearer token
if (!$request->hasHeader('Authorization')) {
return $this->unauthorized('No Authorization header found.');
}
$tokens = $request->bearerToken();
$tokens = explode('|', $tokens);
if (count($tokens) !== 2) {
return $this->unauthorized('Invalid Authorization header.');
}
$whmcs_id = $tokens[0];
$token = $tokens[1];
$whmcs = config('whmcs.' . $whmcs_id);
if (is_null($whmcs)) {
return $this->unauthorized('Invalid WHMCS ID.');
}
$config_token = config('whmcs.' . $whmcs_id . '.api_token');
if ($config_token == null) {
return $this->unauthorized('Token not allowed.');
}
if ($token !== $config_token) {
return $this->unauthorized('Invalid token.');
}
if ($request->user_id) {
$user = User::where('id', $request->user_id)->first();
// if user null
if (!$user) {
$http = Http::remote('remote')->asForm();
$user = $http->get('/users/' . $request->user_id)->json();
$user = User::create([
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
]);
}
Auth::guard('user')->login($user);
}
return $next($request);
}
public function unauthorized($message = 'Unauthorized.')
{
return response()->json([
'message' => $message,
], 401);
}
}

140
app/Support/WHMCS.php Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace App\Support;
class WHMCS
{
private string $url;
private string $username;
private string $password;
private string $api_token;
public int|string $platform;
public array $payments = [];
private string $config_key = '';
private array $config = [];
public function __construct(int|string $whmcs_id)
{
$this->config_key = 'whmcs.' . $whmcs_id;
$whmcs = config($this->config_key);
$this->config = $whmcs;
$this->platform = $whmcs_id;
if (is_null($whmcs)) {
throw new \Exception('WHMCS config not found');
}
$this->url = $whmcs['url'];
$this->username = $whmcs['username'] ?? '';
$this->password = md5($whmcs['password'] ?? '');
$this->api_token = $whmcs['api_token'];
$this->payments = $whmcs['payments'] ?? [];
}
public function hasPayment(string $payment): bool
{
return in_array($payment, array_keys($this->payments));
}
public function getPayments(): array
{
$results = [];
// 获取 config 的所有的 key
$payments = array_keys($this->config['payments'] ?? []);
foreach ($payments as $payment) {
$results[] = [
'name' => $payment,
'title' => $this->config['payments'][$payment]
];
}
return $results;
}
private function request($action, $params = []): ?array
{
$params = array_merge([
'action' => $action,
'username' => $this->username,
'password' => md5($this->password),
'responsetype' => 'json',
], $params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url . '/includes/api.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
http_build_query($params)
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
$json = json_decode($response, true);
throw_if(
is_null($json),
new \Exception('WHMCS response is not valid JSON')
);
throw_if(
isset($json['result']) && $json['result'] !== 'success',
new \Exception($json['message'])
);
return $json;
}
public function api($action, $params = []): ?array
{
$params = array_merge([
'api_token' => $this->api_token
], $params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url . '/modules/addons/PortIOInvoice/api/' . $action . '.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
http_build_query($params)
);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
$json = json_decode($response, true);
throw_if(
is_null($json),
new \Exception('WHMCS response is not valid JSON')
);
throw_if(
isset($json['status']) && $json['status'] !== true,
new \Exception($json['message'] ?? '未知错误')
);
return $json;
}
public function api_addTraffic(string $email, string $payment, int|float $traffic, int|float|string $price)
{
return $this->api('addTraffic', [
'email' => $email,
'traffic' => $traffic,
'price' => $price,
'platform' => $this->platform,
'payment' => $payment
]);
}
}

View File

@ -4,5 +4,6 @@
'sign' => [ 'sign' => [
'max' => 10, 'max' => 10,
'min' => 1, 'min' => 1,
] ],
'price_per_gb' => "0.01",
]; ];

12
config/whmcs.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'test' => [
'url' => 'http://whmcs.test',
'api_token' => 'abc123456',
'payments' => [
'laeFastPay' => '莱云 快捷支付',
'mailin' => '邮入',
]
]
];

View File

@ -37,17 +37,22 @@ const items = ref([
route: "index", route: "index",
}, },
{ {
name: "签到", name: "穿透隧道",
route: "sign",
},
{
name: "隧道",
route: "tunnels", route: "tunnels",
}, },
{ {
name: "创建隧道", name: "创建隧道",
route: "tunnels.create", route: "tunnels.create",
}, },
{
name: "签到",
route: "sign",
},
{
name: "充值",
route: "charge",
},
{ {
name: "客户端下载", name: "客户端下载",
route: "downloads", route: "downloads",

View File

@ -51,6 +51,14 @@ const routes = [
title: "签到", title: "签到",
}, },
}, },
{
path: "/charge",
name: "charge",
component: () => import("../views/Charge.vue"),
meta: {
title: "流量充值",
},
},
]; ];

View File

@ -0,0 +1,155 @@
<template>
<div>
<h3>流量充值</h3>
</div>
<div>
<h5>您要充值多少元的流量</h5>
<p>
GB 价格: <span>{{ price_per_gb }}</span>
</p>
<div class="input-group mb-3">
<input
autofocus
type="number"
class="form-control"
placeholder="输入您要的流量 (单位: GB)"
v-model="amount"
/>
<div class="input-group-append">
<span class="input-group-text">GB</span>
</div>
</div>
<div v-if="amount">
<p>大约 <span v-text="amount * price_per_gb"></span> </p>
<div v-if="providers">
<h5 class="mt-3">您将要使用哪个平台充值</h5>
<p>如果您在选中的平台没有账号我们将会帮您自动创建一个</p>
<template v-for="p in providers">
<div class="form-group form-check">
<input
type="radio"
class="form-check-input"
name="provider"
:id="'providers_' + p"
:value="p"
v-model.value="provider"
@change="getPayments"
/>
<label
v-text="p"
class="form-check-label"
:for="'providers_' + p"
></label>
</div>
</template>
</div>
<div v-else>
<h5 class="mt-3">暂时没有可用的</h5>
</div>
<div v-if="payments">
<h5 class="mt-3">让我们来选择支付方式</h5>
<p>在支付后您的流量大概需要数秒钟到账</p>
<template v-for="py in payments">
<div class="form-group form-check">
<input
type="radio"
class="form-check-input"
name="payment"
:id="'payments_' + py.name"
v-model="payment"
:value="py.name"
/>
<label
v-text="py.title"
class="form-check-label"
:for="'payments_' + py.name"
></label>
</div>
</template>
</div>
<div v-if="payment">
<button
class="btn btn-primary mt-3"
@click="pay"
v-text="loading ? '请稍后' : '立即支付'"
:disabled="loading"
></button>
</div>
</div>
</div>
<p v-if="loading">正在创建订单...</p>
<div v-if="link" class="mt-3">
<h5>完成</h5>
<p>如果您浏览器没有打开新的创建请点击以下链接来打开</p>
<a :href="link" class="link" target="_blank">支付</a>
</div>
</template>
<script setup>
import { ref } from "vue";
import http from "../plugins/http";
const price_per_gb = ref(0);
const providers = ref([]);
const provider = ref("");
const payments = ref({});
const payment = ref("");
const amount = ref(10);
const link = ref("");
const loading = ref(false);
http.get("price").then((res) => {
price_per_gb.value = res.data.price_per_gb;
});
http.get("providers").then((res) => {
providers.value = res.data;
// ()
if (providers.value.length > 0) {
provider.value = providers.value[0];
getPayments();
}
});
function getPayments() {
http.get("providers/" + provider.value + "/payments").then((res) => {
payments.value = res.data;
// ()
if (payments.value.length > 0) {
payment.value = payments.value[0].name;
}
});
}
function pay() {
loading.value = true;
http.post("providers/" + provider.value + "/charge", {
payment: payment.value,
traffic: amount.value,
})
.then((res) => {
link.value = res.data.redirect_url;
setTimeout(() => {
window.open(link.value, "_blank");
});
})
.finally(() => {
loading.value = false;
});
}
</script>

View File

@ -23,9 +23,13 @@
Route::get('traffic', [TrafficController::class, 'free']); Route::get('traffic', [TrafficController::class, 'free']);
Route::post('traffic', [TrafficController::class, 'sign']); Route::post('traffic', [TrafficController::class, 'sign']);
Route::get('price', [TrafficController::class, 'price']);
Route::get('providers', [TrafficController::class, 'providers']);
Route::get('providers/{provider}/payments', [TrafficController::class, 'payments']);
Route::post('providers/{provider}/charge', [TrafficController::class, 'charge']);
}); });
Route::prefix('application')->name('application.')->middleware('api_token')->group(function () { Route::prefix('application')->name('application.')->middleware('whmcs_api')->group(function () {
Route::post('users/{user:email}/traffic', [ApplicationUserController::class, 'addTraffic']); Route::post('users/{user:email}/traffic', [ApplicationUserController::class, 'addTraffic']);
}); });