fuc/src/Services/Cron.php
2025-06-14 14:18:06 +08:00

1272 lines
52 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Ann;
use App\Models\Config;
use App\Models\DetectLog;
use App\Models\EmailQueue;
use App\Models\HourlyUsage;
use App\Models\Invoice;
use App\Models\Node;
use App\Models\OnlineLog;
use App\Models\Order;
use App\Models\Paylist;
use App\Models\SubscribeLog;
use App\Models\User;
use App\Models\TgStep;
use App\Models\Zhuanfas;
use App\Models\Ducks;
use App\Models\Visitor;
use App\Models\CountVisitor;
use App\Models\CountUser;
use App\Models\Product;
use App\Services\TomTool\TgSendFormat;
use App\Services\IM\Telegram;
use App\Services\Tg\ReportUser;
use App\Utils\Tools;
use DateTime;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Client\ClientExceptionInterface;
use Telegram\Bot\Exceptions\TelegramSDKException;
use function array_map;
use function date;
use function in_array;
use function json_decode;
use function str_replace;
use function strtotime;
use function time;
use const PHP_EOL;
final class Cron
{
// 数据库清理
public static function cleanDb(): void
{
(new SubscribeLog())->where(
'request_time',
'<',
time() - 86400 * Config::obtain('subscribe_log_retention_days')
)->delete();
(new HourlyUsage())->where(
'date',
'<',
date('Y-m-d', time() - 86400 * Config::obtain('traffic_log_retention_days'))
)->delete();
(new DetectLog())->where('datetime', '<', time() - 86400 * 3)->delete();
(new EmailQueue())->where('time', '<', time() - 86400)->delete();
(new OnlineLog())->where('last_time', '<', time() - 86400)->delete();
echo Tools::toDateTime(time()) . ' 数据库清理完成' . PHP_EOL;
}
// 清理tg step暂时没用
public static function clearTgStep(): void
{
TgStep::where("date", "<=", date('Y-m-d', strtotime('-1 day')))->delete();
}
// 统计用户数据入库发送tg
public static function userCount(): void
{
$sPrevDate = date("Y-m-d", strtotime("-1 day"));
$sPrevDateStart = date("Y-m-d 00:00:00", strtotime("-1 day"));
$sPrevDateEnd = date("Y-m-d 23:59:59", strtotime("-1 day"));
$sPrevTimeStart = strtotime($sPrevDateStart);
$sPrevTimeEnd = strtotime($sPrevDateEnd);
$sPrevDate30 = date("Y-m-d", strtotime("-30 day"));
//// 访客统计 start
$aVisitorCount = Visitor::select('from', DB::raw('count(*) as count'))
->groupBy('from')
->get()
->toArray();
$aVisitorMix = [];
foreach ($aVisitorCount as $row) {
$aVisitorMix[$row['from']]['count'] = $row['count'];
}
//// 访客统计 end
// 注册数量
$aRegCount = User::select('from', DB::raw('count(*) as count'))
->whereDate('reg_date', $sPrevDate)
->groupBy('from')
->get()
->toArray();
$aRegMix = [];
foreach ($aRegCount as $row) {
if (isset($row['from'])) {
$sFrom = $row['from'];
} else {
$sFrom = '无';
}
$aRegMix[$sFrom]['count'] = $row['count'];
}
$aOrder = Order::whereBetween("update_time", [$sPrevTimeStart, $sPrevTimeEnd])
->where("status", "activated")
->get()
->toArray();
// 标记是否是新用户
$oUserModel = new User();
$aUserMix = [];
foreach ($aOrder as $row) {
$oUser = $oUserModel->where("id", $row["user_id"])->first();
if (isset($oUser->from)) {
$sFrom = $oUser->from;
} else {
$sFrom = "";
}
// 初始化用户数据
if (!isset($aUserMix[$sFrom])) {
$aUserMix[$sFrom] = [
'pay_count_new' => 0,
'money_count_new' => 0,
'pay_count_old' => 0,
'money_count_old' => 0,
'pay_count' => 0,
'money_count' => 0,
];
}
// 确保时间比较的正确性
if (isset($oUser->reg_date) && ($row['update_time'] - strtotime($oUser->reg_date)) < (86400 * 30)) {
$aUserMix[$sFrom]['pay_count_new']++;
$aUserMix[$sFrom]['money_count_new'] += $row["price"];
} else {
$aUserMix[$sFrom]['pay_count_old']++;
$aUserMix[$sFrom]['money_count_old'] += $row["price"];
}
$aUserMix[$sFrom]['pay_count']++;
$aUserMix[$sFrom]['money_count'] += $row["price"];
}
$aAllMix = array_merge($aVisitorMix, $aRegMix, $aUserMix);
foreach ($aAllMix as $k => $v) {
$oCountUserModel = new CountUser();
$oCountUserModel->from = $k;
$oCountUserModel->count_from = $aVisitorMix[$k]['count'] ?? 0;
$oCountUserModel->count_reg = $aRegMix[$k]['count'] ?? 0;
$oCountUserModel->count_pay = $aUserMix[$k]['pay_count'] ?? 0;
$oCountUserModel->count_pay_new = $aUserMix[$k]['pay_count_new'] ?? 0;
$oCountUserModel->count_pay_old = $aUserMix[$k]['pay_count_old'] ?? 0;
$oCountUserModel->count_money = $aUserMix[$k]['money_count'] ?? 0;
$oCountUserModel->count_money_new = $aUserMix[$k]['money_count_new'] ?? 0;
$oCountUserModel->count_money_old = $aUserMix[$k]['money_count_old'] ?? 0;
$oCountUserModel->date = $sPrevDate;
$r = $oCountUserModel->save();
}
Visitor::truncate();
$sMsg = (new ReportUser())->ByDay(1);
(new Telegram())->send(0, $sMsg);
}
// public static function visitorCount(): void
// {
// $oVisitor = Visitor::select('from', DB::raw('count(*) as count'))
// ->groupBy('from')
// ->get();
//
// foreach ($oVisitor as $row) {
//
// $oCountVisitor = new CountVisitor();
// $oCountVisitor->from = $row->from;
// $oCountVisitor->count = $row->count;
// $oCountVisitor->date = date('Y-m-d', strtotime('-1 day'));
// $oCountVisitor->save();
//
// }
//
// Visitor::truncate();
//
// // tg报表
// $aCountVisitor = CountVisitor::where('date', date('Y-m-d', strtotime('-1 day')))->get()->toArray();
//
// $sTgReport = '';
// foreach ($aCountVisitor as $row) {
//
// $sTgReport .= $row["from"]."".$row["count"]."\n";
//
// }
//
// (new Telegram())->send(0, $sTgReport);
//
// }
// 限制帐户检测
public static function detectInactiveUser(): void
{
$checkin_days = Config::obtain('detect_inactive_user_checkin_days');
$login_days = Config::obtain('detect_inactive_user_login_days');
$use_days = Config::obtain('detect_inactive_user_use_days');
(new User())->where('is_admin', 0)
->where('is_inactive', 0)
->where('last_check_in_time', '<', time() - 86400 * $checkin_days)
->where('last_login_time', '<', time() - 86400 * $login_days)
->where('last_use_time', '<', time() - 86400 * $use_days)
->update(['is_inactive' => 1]);
(new User())->where('is_admin', 0)
->where('is_inactive', 1)
->where('last_check_in_time', '>', time() - 86400 * $checkin_days)
->where('last_login_time', '>', time() - 86400 * $login_days)
->where('last_use_time', '>', time() - 86400 * $use_days)
->update(['is_inactive' => 0]);
echo Tools::toDateTime(time()) .
' 检测到 ' . (new User())->where('is_inactive', 1)->count() . ' 个账户处于闲置状态' . PHP_EOL;
}
// 节点掉线检测
public static function detectNodeOffline(): void
{
$nodes = (new Node())->where('type', 1)->get();
foreach ($nodes as $node) {
if ($node->getNodeOnlineStatus() >= 0 && $node->online === 1) {
continue;
}
if ($node->getNodeOnlineStatus() === -1 && $node->online === 1) {
echo 'Send Node Offline Email to admin users' . PHP_EOL;
try {
Notification::notifyAdmin(
$_ENV['appName'] . '-系统警告',
'管理员你好,系统发现节点 ' . $node->name . ' 掉线了,请你及时处理。'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
if (Config::obtain('telegram_node_offline')) {
$notice_text = str_replace(
'%node_name%',
$node->name,
Config::obtain('telegram_node_offline_text')
);
try {
(new Telegram())->send(0, $notice_text);
} catch (TelegramSDKException $e) {
echo $e->getMessage();
}
}
$node->online = 0;
$node->save();
continue;
}
if ($node->getNodeOnlineStatus() === 1 && $node->online === 0) {
echo 'Send Node Online Email to admin user' . PHP_EOL;
try {
Notification::notifyAdmin(
$_ENV['appName'] . '-系统提示',
'管理员你好,系统发现节点 ' . $node->name . ' 恢复上线了。'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
if (Config::obtain('telegram_node_online')) {
$notice_text = str_replace(
'%node_name%',
$node->name,
Config::obtain('telegram_node_online_text')
);
try {
(new Telegram())->send(0, $notice_text);
} catch (TelegramSDKException $e) {
echo $e->getMessage();
}
}
$node->online = 1;
$node->save();
}
}
echo Tools::toDateTime(time()) . ' 节点离线检测完成' . PHP_EOL;
}
// 新增,专门检测处理体验用户
// class=1为免费体验用户免费体验到期转为0彻底不能用
public static function expireFreeUserAccount(): void
{
$paidUsers = (new User())->where('class', 1)->get();
foreach ($paidUsers as $user) {
if (strtotime($user->class_expire) < time()) {
$user->u = 0;
$user->d = 0;
$user->transfer_today = 0;
$user->class = 0;
$user->save();
}
}
echo Tools::toDateTime(time()) . ' 体验用户过期检测完成' . PHP_EOL;
}
// 付费用户过期检测
// 现在付费用户是2+体验用户是1。付费到期转为体验。
public static function expirePaidUserAccount(): void
{
$paidUsers = (new User())->where('class', '>=', 2)->get();
foreach ($paidUsers as $user) {
if (strtotime($user->class_expire) < time()) {
$text = '您好,系统发现您的套餐已经过期了。';
$reset_traffic = $_ENV['class_expire_reset_traffic'];
$reset_day = $_ENV['class_expire_reset_day'];
if ($reset_traffic >= 0) {
$user->transfer_enable = Tools::toGB($reset_traffic);
$user->class_expire = date('Y-m-d H:i:s', strtotime("+$reset_day days"));
$text .= '已转为免费体验帐户,赠送' . $reset_traffic . 'GB体验流量'.$reset_day.'天体验时长。';
}
// todo 反水
// 这里返水的话,等于是直接覆盖的用户不返水,自然过期的用户返水,这到合理,自然过期的才是好用户
try {
Notification::notifyUser($user, $_ENV['appName'] . '-您的套餐已经过期', $text);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
$user->u = 0;
$user->d = 0;
$user->transfer_today = 0;
$user->class = 1;
$user->save();
}
//// 流量到期处理 不重置了,默认暂停就行
// 这里只重置付费用户不重置免费用户。那么假如免费用户领了60g流量他就真的能用到没。付费用户则会连带套餐一起重置掉。
// 应该问题不大,赠送流量有关时,区分免费付费用户,对付费大方(陷阱),对免费抠门。
// 其实压根就不叠加,套餐流量直接覆盖免费流量。
// 再者,免费流量
// 总之,免费流量就是零头,无所谓重置的。
// 用户流量用光,或者用户等级到期,就重置。
// 流量用光时顺便标记订单。在这里标记不容易埋坑。
// $ud = $user->u + $user->d;
// if ($ud >= $user->transfer_enable) {
//
// // 订单标记为过期
// // 既然class大于0就说明肯定有激活的套餐对吧
// $activated_order = (new Order())->where('user_id', $user->id)
// ->where('status', 'activated')
// ->where('product_type', 'tabp')
// ->orderBy('id')
// ->first();
//
// // 以防万一,判断一下
// if (!$activated_order) {
// $activated_order->status = 'expired';
// $activated_order->update_time = time();
// $activated_order->save();
// }
//
//// tomd($activated_order, 1);
// // 用户流量重置
// $user->u = 0;
// $user->d = 0;
// $user->class = 0;
// $user->transfer_today = 0;
// $user->transfer_enable = 0;
// $user->class_expire = date("Y-m-d H:i:s"); // 用户等级时间到当前,注意订单没变,应该合理
// $user->save();
//
// $sText = "你好,系统发现您的流量已用完,已转为免费体验帐户,可用每日签到获取免费流量。";
//
// try {
// Notification::notifyUser($user, $_ENV['appName'] . '-您的流量已用完', $sText);
// } catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
// echo $e->getMessage() . PHP_EOL;
// }
//
// }
//// end
}
echo Tools::toDateTime(time()) . ' 付费用户过期检测完成' . PHP_EOL;
}
// 邮件队列处理
public static function processEmailQueue(): void
{
if ((new EmailQueue())->count() === 0) {
echo Tools::toDateTime(time()) . ' 邮件队列为空' . PHP_EOL;
} else {
//记录当前时间戳
$timestamp = time();
//邮件队列处理
while (true) {
if (time() - $timestamp > 299) {
echo Tools::toDateTime(time()) . '邮件队列处理超时,已跳过' . PHP_EOL;
break;
}
DB::beginTransaction();
$email_queues_raw = DB::select('SELECT * FROM email_queue LIMIT 1 FOR UPDATE SKIP LOCKED');
if (count($email_queues_raw) === 0) {
DB::commit();
break;
}
$email_queues = array_map(static function ($value) {
return (array) $value;
}, $email_queues_raw);
$email_queue = $email_queues[0];
echo '发送邮件至 ' . $email_queue['to_email'] . PHP_EOL;
DB::delete('DELETE FROM email_queue WHERE id = ?', [$email_queue['id']]);
if (Tools::isEmail($email_queue['to_email'])) {
try {
Mail::send(
$email_queue['to_email'],
$email_queue['subject'],
$email_queue['template'],
json_decode($email_queue['array'])
);
} catch (Exception|ClientExceptionInterface $e) {
echo $e->getMessage();
}
} else {
echo $email_queue['to_email'] . ' 邮箱格式错误,已跳过' . PHP_EOL;
}
DB::commit();
}
echo Tools::toDateTime(time()) . ' 邮件队列处理完成' . PHP_EOL;
}
}
// 激活订单
/**
逻辑:
查询,遍历所有用户
根据uid找待激活订单
根据uid找已激活订单
判断:当有待激活订单时:
判断:无已激活订单,激活
判断:此用户流量满了,激活。就加了这个。
无待激活订单时,确定不激活
流量没满时,走原来的逻辑,无已激活时激活
判断:有已激活订单
判断:订单的等级时间过期,标记过期
*/
public static function processTabpOrderActivation(): void
{
$users = User::all();
foreach ($users as $user) {
$user_id = $user->id;
// 获取用户账户等待激活的TABP订单
$pending_activation_orders = (new Order())->where('user_id', $user_id)
->where('status', 'pending_activation')
->where('product_type', 'tabp')
->orderBy('id')
->get();
// 获取用户账户已激活的TABP订单一个用户同时只能有一个已激活的TABP订单
$activated_order = (new Order())->where('user_id', $user_id)
->where('status', 'activated')
->where('product_type', 'tabp')
->orderBy('id')
->first();
/*
添加条件,当流量用满时,可以直接激活新套餐。
那如果,没有新套餐,就等于旧套餐被暂停了,等到用户等级过期时,走原有的重置,没错。
那如果,有新套餐,那就是直接覆盖,刷新。
也就是说,用户流量不满时,就是当前套餐到期再激活新套餐。
而用户流量满时,就是直接激活新套餐。
这里是有点怪,两个玩法。何不直接叠加?
如果都直接叠加,会怎样?
连充12个月会员直接是一年好像没问题之前为什么否了
好像是因为续流量比较亏,因为时间是虚的而流量是实的。
但这其实没办法,不管用什么方案,流量满了总得允许人家续,没得变。
根本区别就是,叠加有粘性,不叠加省成本。
用户流量满了,他就叠流量,最后时间剩很多,流量没有,套餐就成了流量包。
“我套餐还没到期,但是流量已经没了,无所谓”,所以这里不是留住客户的理由。
用户时间到了,他就叠时间,最后流量用不完,时间快到期了。
那基本上他会狂用看4k被。
“我流量还没用完呢,续一下”
“这么多流量,看高清”
理想情况,时间叠褶叠着,流量用不完,然后时间到期,一下清空。
这种情况,应该不会太多,他流量多自然就不会省着用了。
所以,还是别叠加,留住客户可以返水之类的,而不是在这里暗着给他们实惠。
不叠加指的是时间不叠加,流量还是要叠的,这没有其他办法。
顶多是流量满了直接重置时间,但这想想没必要。时间是虚的,重置不重置没什么区别,倒是产生业务上的混乱感。
完全没必要,流量满了其实就暂停了,再买套餐就覆盖。重置也相当于暂停,再买套餐也是覆盖,都一样。
之前的逻辑怪圈,就是他原本用户等级到期会重置,我就想当然的做成流量满了也重置。
首先他为什么原本到期重置
因为订单function只影响订单状态不影响用户状态他俩是独立的。
if订单到期修改。if用户等级到期修改。代码上是各玩各的只是刚好订单和用户等级的时间是一致的所以会同时处理。
你被误导的地方是有可激活订单时,会去覆盖用户。就仿佛他俩是直接关联的。
但要是没有可激活订单呢?用户就一直不重置了,到流量用完暂停。这就是要有用户重置的原因。
用户重置是基础,订单的重置是补偿。
那流量为什么不能重置
因为没必要,反而导致业务逻辑奇怪。
为什么奇怪用户原本是20号到期流量用光变成了10号到期他会迷茫。
用户10号到期也就算了订单状态也跟着变了提前到期了。就很怪。
所以就不重置它,保持暂停就行了。等待时间到期走原本的重置。
还有一个点webapi那边会判断流量满了暂停而不会判断等级到期暂停所以要有用户时间重置不然就会跑到流量用光为止。
所以流量满了可以不重置,会自然暂停。
最后确定一下,到底行不行,别又弄错了。
现在就是说,只要流量满了,就直接激活待激活套餐,即便还存在已激活订单。
就会同时会出现两个激活套餐,这倒没事,只要代码没有要求只读一个激活套餐。到时检查一下。
其他地方都不用动,爱加就加个流量提醒(本来就有)。对吗?
对,就是这样,流量满了就允许直接激活覆盖新套餐,就这个逻辑。
实在不放心,就把当前订单标记成过期。
没问题,确实没问题,就这么干。
再缕一次
起初,发现流量满了不能激活套餐(原本是用流量包续)
我最初是重置了套餐满了的用户流量但没重置订单状态。幸亏是只重置class大于0不会有大的损失。
然后想到应该标记订单,当前订单标记成过期,下一个订单就能激活了。
至于用户function那边应该不用重置它自然暂停即可。
然后又想到,那会导致业务逻辑怪异,所以就在这加个判断,流量满时直接激活下一个订单,不考虑是否有已激活。这个问题也就解决了。
所以就是说,我要解决《用户流量满了不能激活新套餐的问题》,我就在这里加个判断,逻辑是《流量满了直接激活新套餐,即便存在已激活订单》。
订单状态有几种?
待激活 - 必须条件
已激活 - 附加条件A反向
取消 - 不相关,必然不进
等待付款 - 不相关,必然不进
也就是
如果有待激活,就直接激活,不管是否有已激活
如果没有待激活,不管。自然暂停,自然重置。
没有其他情况,对吗?
两种条件会激活:
没有已激活订单,只有待激活订单
有激活订单,用户流量满了
其他条件不会激活?
那如果,没有已激活订单,同时用户流量满了呢?
也会走进来
那无所谓,是按原有逻辑激活
没有待激活是肯定不会进来的
换个说法就是,有待激活,并且没有已激活或者用户流量满了,就进来
激活的必须条件是有待激活订单
然后没有已激活,或者用户流量满了
可能的条件(情况):
有待激活订单,用户流量满 - 已测试
有待激活订单,无已激活订单 - 已测试
假如用户没有已激活套餐,却有等级流量呢
当然是不用管他,活动获取的呗
有待激活订单,用户流量满,无已激活订单
没问题,等于走原来的逻辑
不可能的情况:
有待激活套餐,有激活套餐,用户流量不满 - 已测试
没有待激活套餐 - 不可能
*/
// 如果用户账户中没有已激活的TABP订单且有等待激活的TABP订单则激活最早的等待激活TABP订单
// * 如果用户流量已满,就直接激活待激活订单,不考虑是否有已激活。(之前想的是流量满时把订单标记成过期,这虽然也可以,但是业务逻辑会显得奇怪)
$ud = $user->u + $user->d;
// 待激活订单 > 0 && (没有已激活订单 || 用户流量满)
if (count($pending_activation_orders) > 0 && ($activated_order === null || $ud >= $user->transfer_enable)) {
// if ($activated_order === null && count($pending_activation_orders) > 0) {
// if (($activated_order === null && count($pending_activation_orders) > 0) || ($ud > $user->transfer_enable && count($pending_activation_orders) > 0)) {
$order = $pending_activation_orders[0];
// 获取TABP订单内容准备激活
$content = json_decode($order->product_content);
//// * 是用户流量满时,主动标记订单过期
// 按理说可以有多个已激活,但是以防万一,标记掉
// 假如等级还过期了,就是标记两次,无所谓
if ($ud >= $user->transfer_enable && $activated_order !== null) {
$activated_order->status = 'expired';
$activated_order->update_time = time();
$activated_order->save();
echo "TABP订单 #{$activated_order->id} 已过期(流量满了)。\n";
}
//// end
//// * 特别活动追加,季套餐赠月套餐,年套餐赠季套餐
if ($order->huodong_class && $order->huodong_class == "A") {
// 按理是大于30天做个冗余
if ($content->class_time > 32) {
$oProduct = Product::where("name", $order->product_name)
->where("type_by_time", "<", $content->class_time)
->orderBy("type_by_time", "desc")
->first();
if ($oProduct) {
$oProductContent = json_decode($oProduct->content);
$content->class_time += $oProductContent->class_time;
$content->bandwidth += $oProductContent->bandwidth;
}
}
}
//// end
// 激活TABP
// 等级时间,流量,都是覆盖刷新,不是叠加
$user->u = 0;
$user->d = 0;
$user->transfer_today = 0;
$user->transfer_enable = Tools::toGB($content->bandwidth);
$user->class = $content->class;
$old_class_expire = new DateTime();
$user->class_expire = $old_class_expire
->modify('+' . $content->class_time . ' days')->format('Y-m-d H:i:s'); // 当前时间+订单等级时间
$user->node_group = $content->node_group;
$user->node_speedlimit = $content->speed_limit;
$user->node_iplimit = $content->ip_limit;
$user->save();
$order->status = 'activated';
$order->update_time = time();
$order->save();
echo "TABP订单 #{$order->id} 已激活。\n";
continue;
}
// 如果用户账户中有已激活的TABP订单则判断是否过期
if ($activated_order !== null) {
$content = json_decode($activated_order->product_content);
// 原来用的是$content->time
if ($activated_order->update_time + $content->class_time * 86400 < time()) {
$activated_order->status = 'expired';
$activated_order->update_time = time();
$activated_order->save();
echo "TABP订单 #{$activated_order->id} 已过期。\n";
}
}
}
echo Tools::toDateTime(time()) . ' TABP订单激活处理完成' . PHP_EOL;
}
// 流量包激活,暂时没用
public static function processBandwidthOrderActivation(): void
{
$users = User::all();
foreach ($users as $user) {
$user_id = $user->id;
// 获取用户账户等待激活的流量包订单
$order = (new Order())->where('user_id', $user_id)
->where('status', 'pending_activation')
->where('product_type', 'bandwidth')
->orderBy('id')
->first();
if ($order !== null) {
// 获取流量包订单内容准备激活
$content = json_decode($order->product_content);
// 激活流量包
$user->transfer_enable += Tools::toGB($content->bandwidth);
$user->save();
$order->status = 'activated';
$order->update_time = time();
$order->save();
echo "流量包订单 #{$order->id} 已激活。\n";
}
}
echo Tools::toDateTime(time()) . ' 流量包订单激活处理完成' . PHP_EOL;
}
// 时间包激活,暂时没用
public static function processTimeOrderActivation(): void
{
$users = User::all();
foreach ($users as $user) {
$user_id = $user->id;
// 获取用户账户等待激活的时间包订单
$order = (new Order())->where('user_id', $user_id)
->where('status', 'pending_activation')
->where('product_type', 'time')
->orderBy('id')
->first();
if ($order !== null) {
$content = json_decode($order->product_content);
// 跳过当前账户等级不等于时间包等级的非免费用户订单
if ($user->class !== (int) $content->class && $user->class > 0) {
continue;
}
// 激活时间包
$user->class = $content->class;
$old_class_expire = new DateTime($user->class_expire);
$user->class_expire = $old_class_expire
->modify('+' . $content->class_time . ' days')->format('Y-m-d H:i:s');
$user->node_group = $content->node_group;
$user->node_speedlimit = $content->speed_limit;
$user->node_iplimit = $content->ip_limit;
$user->save();
$order->status = 'activated';
$order->update_time = time();
$order->save();
echo "时间包订单 #{$order->id} 已激活。\n";
}
}
echo Tools::toDateTime(time()) . ' 时间包订单激活处理完成' . PHP_EOL;
}
// 标记订单
public static function processPendingOrder(): void
{
$pending_payment_orders = (new Order())->where('status', 'pending_payment')->get();
foreach ($pending_payment_orders as $order) {
// 检查账单支付状态
$invoice = (new Invoice())->where('order_id', $order->id)->first();
if ($invoice === null) {
continue;
}
// 标记订单为等待激活
if (in_array($invoice->status, ['paid_gateway', 'paid_balance', 'paid_admin'])) {
$order->status = 'pending_activation';
$order->update_time = time();
$order->save();
echo "已标记订单 #{$order->id} 为等待激活。\n";
continue;
}
// 取消超时未支付的订单和关联账单
if ($order->create_time + 86400 < time()) {
$order->status = 'cancelled';
$order->update_time = time();
$order->save();
echo "已取消超时订单 #{$order->id}\n";
$invoice->status = 'cancelled';
$invoice->update_time = time();
$invoice->save();
echo "已取消超时账单 #{$invoice->id}\n";
}
}
echo Tools::toDateTime(time()) . ' 等待中订单处理完成' . PHP_EOL;
}
public static function removeInactiveUserLinkAndInvite(): void
{
$inactive_users = (new User())->where('is_inactive', 1)->get();
foreach ($inactive_users as $user) {
$user->removeLink();
$user->removeInvite();
}
echo Tools::toDateTime(time()) . ' Successfully removed inactive user\'s Link and Invite' . PHP_EOL;
}
// 废弃
public static function resetNodeBandwidth(): void
{
(new Node())->where('bandwidthlimit_resetday', date('d'))->update(['node_bandwidth' => 0]);
echo Tools::toDateTime(time()) . ' 重设节点流量完成' . PHP_EOL;
}
// 机器到日期重置流量
public static function resetDuckBandwidth(): void
{
(new Ducks())->where('width_reset_day', date('d'))->update(['use_width' => 0]);
echo Tools::toDateTime(time()) . ' 重设机器流量完成' . PHP_EOL;
}
// public static function resetZhuanfaBandwidth(): void
// {
// (new Ducks())->where('end_date', date('Y-m-d'))->update(['use_width' => 0]);
//
// echo Tools::toDateTime(time()) . ' 重设机器流量完成' . PHP_EOL;
// }
// 每小时统计节点流量入库tg警报
public static function mergeBandWidth(): void
{
$oNodeAll = Node::where('type', 1)->get();
$aZhuanfaFlowNow = [];
$aDuckFlowNow = [];
$sMsg = "";
// 统计node流量
foreach ($oNodeAll as $oNodeRow) {
if ($oNodeRow->node_bandwidth == 0) {
$sMsg .= '🚨【'.$oNodeRow->name."】小时未耗流,可能异常。\n";
}
@$aZhuanfaFlowNow[$oNodeRow->zhuanfa_code] += $oNodeRow->node_bandwidth;
@$aDuckFlowNow[$oNodeRow->duck_code] += $oNodeRow->node_bandwidth;
}
// 入库zhunafa
foreach ($aZhuanfaFlowNow as $k => $v) {
if ($v) {
Zhuanfas::where('code', $k)->increment('today_width', $v);
Zhuanfas::where('code', $k)->increment('use_width', $v);
}
}
// 入库duck
foreach ($aDuckFlowNow as $k => $v) {
if ($v) {
Ducks::where('code', $k)->increment('today_width', $v);
Ducks::where('code', $k)->increment('use_width', $v);
}
}
// 清空node流量
Node::query()->update(['node_bandwidth' => 0]);
if ($sMsg) {
// echo $sMsg;
(new Telegram())->send(0, $sMsg);
}
}
// 每日报表
public static function reportBandWidth(): void
{
$oZhuanfas = new Zhuanfas();
$oDucks = new Ducks();
// 流量报告,转发
$oTgSendFormat = new TgSendFormat();
$oTgSendFormat->sTitleEmoji = "\n\n";
$oTgSendFormat->sTitle = "# 转发流量";
$today = new DateTime();
$oZhuanfasAll = $oZhuanfas->all();
foreach ($oZhuanfasAll as $oZhuanfasRow) {
$targetDate = new DateTime($oZhuanfasRow->end_date);
$interval = $today->diff($targetDate);
if ($today > $targetDate) {
$iDiffDay = -$interval->days;
} else {
$iDiffDay = $interval->days;
}
$oTgSendFormat->rowAdd(
$oZhuanfasRow->name,
$oZhuanfasRow->today_width,
$oZhuanfasRow->use_width,
$oZhuanfasRow->max_width,
$iDiffDay
);
}
$sTgReport = $oTgSendFormat->typeA();
// var_dump($sTgReport);
(new Telegram())->send(0, $sTgReport);
// 流量报告,机器
$oTgSendFormat = new TgSendFormat();
$oTgSendFormat->sTitleEmoji = "\n\n";
$oTgSendFormat->sTitle = "# 机器流量";
$oDucksAll = $oDucks->orderBy('code')->get();
foreach ($oDucksAll as $oDucksRow) {
if ($oDucksRow->width_reset_day == 0) {
$iWidthResetDay = 0;
} if ($oDucksRow->width_reset_day > date('d')) {
$iWidthResetDay = $oDucksRow->width_reset_day - date('d');
} else {
$date = new DateTime();
$date->modify('first day of next month');
$date->setDate((int)$date->format('Y'), (int)$date->format('m'), $oDucksRow->width_reset_day);
$sEndDate = $date;
$currentDate = new DateTime();
$interval = $currentDate->diff($sEndDate);
$iWidthResetDay = $interval->days;
}
$oTgSendFormat->rowAdd(
$oDucksRow->code,
$oDucksRow->today_width,
$oDucksRow->use_width,
$oDucksRow->max_width,
$iWidthResetDay,
$oDucksRow->memo
);
}
$sTgReport = $oTgSendFormat->typeA();
// var_dump($sTgReport);
(new Telegram())->send(0, $sTgReport);
}
// 每日清空duck和转发的每日流量
public static function clearTodayWidth(): void
{
Ducks::query()->update(['today_width' => 0]);
Zhuanfas::query()->update(['today_width' => 0]);
}
public static function reportBandWidth_back(): void
{
$oNode = new Node();
$oZhuanfas = new Zhuanfas();
$oDucks = new Ducks();
$oNodeAll = $oNode->all();
foreach ($oNodeAll as $oNodeRow) {
@ $iZhuanfaFlowToday[$oNodeRow->zhuanfa_code] += $oNodeRow->node_bandwidth;
@ $iDuckFlowToday[$oNodeRow->duck_code] += $oNodeRow->node_bandwidth;
$oZhuanfas->where('code', $oNodeRow->zhuanfa_code)->increment('use_width', $oNodeRow->node_bandwidth);
$oDucks->where('code', $oNodeRow->duck_code)->increment('use_width', $oNodeRow->node_bandwidth);
$oNode->where('id', $oNodeRow->id)->update(['node_bandwidth' => 0]);
}
// 流量报告,转发
$oTgSendFormat = new TgSendFormat();
$oTgSendFormat->sTitleEmoji = "⚡️⚡️⚡️⚡️⚡️⚡️";
$oTgSendFormat->sTitle = "# 转发流量";
$today = new DateTime();
$oZhuanfasAll = $oZhuanfas->all();
foreach ($oZhuanfasAll as $oZhuanfasRow) {
$targetDate = new DateTime($oZhuanfasRow->end_date);
$interval = $today->diff($targetDate);
$oTgSendFormat->rowAdd(
$oZhuanfasRow->name,
$iZhuanfaFlowToday[$oZhuanfasRow->code],
$oZhuanfasRow->use_width,
$oZhuanfasRow->max_width,
$interval->days
);
}
$sTgReport = $oTgSendFormat->typeA();
(new Telegram())->send(0, $sTgReport);
// 流量报告,机器
$oTgSendFormat = new TgSendFormat();
$oTgSendFormat->sTitleEmoji = "\n\n";
$oTgSendFormat->sTitle = "# 机器流量";
$oDucksAll = $oDucks->orderBy('code')->get();
foreach ($oDucksAll as $oDucksRow) {
if ($oDucksRow->width_reset_day == 0) {
$iWidthResetDay = 0;
} if ($oDucksRow->width_reset_day > date('d')) {
$iWidthResetDay = $oDucksRow->width_reset_day - date('d');
} else {
$date = new DateTime();
$date->modify('first day of next month');
$date->setDate((int)$date->format('Y'), (int)$date->format('m'), $oDucksRow->width_reset_day);
$sEndDate = $date;
$currentDate = new DateTime();
$interval = $currentDate->diff($sEndDate);
$iWidthResetDay = $interval->days;
}
$oTgSendFormat->rowAdd(
$oDucksRow->code,
$iDuckFlowToday[$oDucksRow->code] ?? 0,
$oDucksRow->use_width,
$oDucksRow->max_width,
$iWidthResetDay
);
}
$sTgReport = $oTgSendFormat->typeA();
(new Telegram())->send(0, $sTgReport);
}
// 用户流量每日重置
public static function resetTodayBandwidth(): void
{
(new User())->query()->update(['transfer_today' => 0]);
echo Tools::toDateTime(time()) . ' 重设用户每日流量完成' . PHP_EOL;
}
// 这是那个每月几日重置
// 是重置免费用户
// 现在没用了,傻逼功能。用户光有流量没有等级照样用不了,取消等级限制则变成所有用户都运作,根本不合理
public static function resetFreeUserBandwidth(): void
{
$freeUsers = (new User())->where('class', 0)
->where('auto_reset_day', date('d'))->get();
foreach ($freeUsers as $user) {
try {
Notification::notifyUser(
$user,
$_ENV['appName'] . '-免费流量重置通知',
'你好,你的免费流量已经被重置为' . $user->auto_reset_bandwidth . 'GB。'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
$user->u = 0;
$user->d = 0;
$user->transfer_enable = $user->auto_reset_bandwidth * 1024 * 1024 * 1024;
$user->save();
}
echo Tools::toDateTime(time()) . ' 免费用户流量重置完成' . PHP_EOL;
}
public static function resetFreeUserBandwidth_new(): void
{
echo 1111;
}
// 财务日报,好像没用
public static function sendDailyFinanceMail(): void
{
$today = strtotime('00:00:00');
$paylists = (new Paylist())->where('status', 1)
->whereBetween('datetime', [strtotime('-1 day', $today), $today])->get();
$text_html = '<table border=1><tr><td>金额</td><td>用户ID</td><td>用户名</td><td>充值时间</td>';
foreach ($paylists as $paylist) {
$text_html .= '<tr>';
$text_html .= '<td>' . $paylist->total . '</td>';
$text_html .= '<td>' . $paylist->userid . '</td>';
$text_html .= '<td>' . (new User())->find($paylist->userid)->user_name . '</td>';
$text_html .= '<td>' . Tools::toDateTime((int) $paylist->datetime) . '</td>';
$text_html .= '</tr>';
}
$text_html .= '</table>';
$text_html .= '<br>昨日总收入笔数:' . count($paylists) . '<br>昨日总收入金额:' . $paylists->sum('total');
echo 'Sending daily finance email to admin user' . PHP_EOL;
try {
Notification::notifyAdmin(
'财务日报',
$text_html,
'finance.tpl'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
echo Tools::toDateTime(time()) . ' 成功发送财务日报' . PHP_EOL;
}
// 财务周报,好像没用
public static function sendWeeklyFinanceMail(): void
{
$today = strtotime('00:00:00');
$paylists = (new Paylist())->where('status', 1)
->whereBetween('datetime', [strtotime('-1 week', $today), $today])
->get();
$text_html = '<br>上周总收入笔数:' . count($paylists) . '<br>上周总收入金额:' . $paylists->sum('total');
echo 'Sending weekly finance email to admin user' . PHP_EOL;
try {
Notification::notifyAdmin(
'财务周报',
$text_html,
'finance.tpl'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
echo Tools::toDateTime(time()) . ' 成功发送财务周报' . PHP_EOL;
}
// 财务越好,好像没用
public static function sendMonthlyFinanceMail(): void
{
$today = strtotime('00:00:00');
$paylists = (new Paylist())->where('status', 1)
->whereBetween('datetime', [strtotime('-1 month', $today), $today])
->get();
$text_html = '<br>上月总收入笔数:' . count($paylists) . '<br>上月总收入金额:' . $paylists->sum('total');
echo 'Sending monthly finance email to admin user' . PHP_EOL;
try {
Notification::notifyAdmin(
'财务月报',
$text_html,
'finance.tpl'
);
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
echo $e->getMessage() . PHP_EOL;
}
echo Tools::toDateTime(time()) . ' 成功发送财务月报' . PHP_EOL;
}
// 流量提醒
public static function sendPaidUserUsageLimitNotification(): void
{
$paidUsers = (new User())->where('class', '>', 0)->get();
foreach ($paidUsers as $user) {
$user_traffic_left = $user->transfer_enable - $user->u - $user->d;
$under_limit = false;
$unit_text = '';
if ($_ENV['notify_limit_mode'] === 'per' &&
$user_traffic_left / $user->transfer_enable * 100 < $_ENV['notify_limit_value']
) {
$under_limit = true;
$unit_text = '%';
} elseif ($_ENV['notify_limit_mode'] === 'mb' &&
Tools::flowToMB($user_traffic_left) < $_ENV['notify_limit_value']
) {
$under_limit = true;
$unit_text = 'MB';
}
if ($under_limit && ! $user->traffic_notified) {
try {
Notification::notifyUser(
$user,
$_ENV['appName'] . '-你的剩余流量过低',
'你好,系统发现你剩余流量已经低于 ' . $_ENV['notify_limit_value'] . $unit_text . ' 。',
);
$user->traffic_notified = true;
} catch (GuzzleException|ClientExceptionInterface|TelegramSDKException $e) {
$user->traffic_notified = false;
echo $e->getMessage() . PHP_EOL;
}
$user->save();
} elseif (! $under_limit && $user->traffic_notified) {
$user->traffic_notified = false;
$user->save();
}
}
echo Tools::toDateTime(time()) . ' 付费用户用量限制提醒完成' . PHP_EOL;
}
public static function sendDailyTrafficReport(): void
{
$users = (new User())->whereIn('daily_mail_enable', [1, 2])->get();
$ann_latest_raw = (new Ann())->orderBy('date', 'desc')->first();
if ($ann_latest_raw === null) {
$ann_latest = '<br><br>';
} else {
$ann_latest = $ann_latest_raw->content . '<br><br>';
}
foreach ($users as $user) {
$user->sendDailyNotification($ann_latest);
}
echo Tools::toDateTime(time()) . ' 成功发送每日流量报告' . PHP_EOL;
}
/**
* @throws TelegramSDKException
*/
public static function sendTelegramDailyJob(): void
{
(new Telegram())->send(0, Config::obtain('telegram_daily_job_text'));
echo Tools::toDateTime(time()) . ' 成功发送 Telegram 每日任务提示' . PHP_EOL;
}
/**
* @throws TelegramSDKException
*/
public static function sendTelegramDiary(): void
{
(new Telegram())->send(
0,
str_replace(
[
'%getTodayCheckinUser%',
'%lastday_total%',
],
[
Analytics::getTodayCheckinUser(),
Analytics::getTodayTrafficUsage(),
],
Config::obtain('telegram_diary_text')
)
);
echo Tools::toDateTime(time()) . ' 成功发送 Telegram 系统运行日志' . PHP_EOL;
}
public static function updateNodeIp(): void
{
$nodes = (new Node())->where('type', 1)->get();
foreach ($nodes as $node) {
$node->updateNodeIp();
$node->save();
}
echo Tools::toDateTime(time()) . ' 更新节点 IP 完成' . PHP_EOL;
}
}