189 lines
7.9 KiB
PHP
189 lines
7.9 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Categories;
|
||
use App\Models\Articles;
|
||
use App\Models\ArticleLikes;
|
||
use App\Models\Tags;
|
||
use App\Services\CacheService;
|
||
|
||
class ArticlesService
|
||
{
|
||
/**
|
||
* 第一步:文章创建时,生成一个“7天点赞计划”
|
||
* 规则:第一天占 70%,剩下 6 天分摊 30%,全部为整数
|
||
*/
|
||
public function generateSevenDayPlan(int $iTargetTotal): string
|
||
{
|
||
if ($iTargetTotal <= 0) return '[]';
|
||
|
||
$aPlan = array_fill(0, 7, 0);
|
||
|
||
// 1. 第一天分配 70% (向上取整,保证第一天绝对有动作)
|
||
$iDay1Likes = (int) ceil($iTargetTotal * 0.7);
|
||
$aPlan[0] = $iDay1Likes;
|
||
|
||
// 2. 剩下的 30% 分配给后面 6 天
|
||
$iRemaining = $iTargetTotal - $iDay1Likes;
|
||
if ($iRemaining > 0) {
|
||
// 随机把剩下的点赞塞进后 6 天的坑位里
|
||
for ($i = 0; $i < $iRemaining; $i++) {
|
||
$iRandomDay = mt_rand(1, 6);
|
||
$aPlan[$iRandomDay]++;
|
||
}
|
||
}
|
||
|
||
// 返回匈牙利命名法下的 json 字符串
|
||
return json_encode($aPlan);
|
||
}
|
||
|
||
/**
|
||
* 第二步:执行点赞任务 (建议每小时跑一次)
|
||
* 只负责把今天该加的赞加进去
|
||
*/
|
||
public function executeLikeTask(Article $oArticle): void
|
||
{
|
||
if (empty($oArticle->sLikePlan)) return;
|
||
|
||
$aPlan = json_decode($oArticle->sLikePlan, true);
|
||
$iDaysPassed = $oArticle->created_at->diffInDays(now());
|
||
|
||
// 如果已经超过7天,或者计划里没这一天,直接撤退
|
||
if ($iDaysPassed > 6 || !isset($aPlan[$iDaysPassed])) {
|
||
return;
|
||
}
|
||
|
||
$iShouldHaveAdded = $aPlan[$iDaysPassed];
|
||
|
||
// 重点:为了不让点赞在凌晨一瞬间加上去,我们可以配合小时数
|
||
// 比如现在是 14 点,我们应该加到今天总量的 14/24
|
||
$iHour = (int) date('H');
|
||
$iCurrentTarget = (int) floor($iShouldHaveAdded * ($iHour / 24));
|
||
|
||
// 这里需要一个逻辑记录今天已经加了多少,或者直接根据小时计算增量
|
||
// 简单处理:我们每天分 24 次把 $iShouldHaveAdded 加完
|
||
// 为保简洁,这里展示核心逻辑更新:
|
||
if ($iShouldHaveAdded > 0) {
|
||
// 算出每小时平均加几个,剩下的随机掉落
|
||
$iAddToday = $this->calculateHourlyIncrement($iShouldHaveAdded, $iHour);
|
||
// 这里更新 i7like 字段
|
||
// 注意:i7like 应该是这篇文章总共通过这个逻辑加了多少赞
|
||
// ... 具体的数据库 Update 逻辑
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 365天自动点赞:高标准随机触发
|
||
*/
|
||
public function applyYearlyGrowth(Article $oArticle): void
|
||
{
|
||
$iYearlyTarget = 10; // 哪怕一年只要 10 个
|
||
|
||
// 计算今天中奖的概率:10 / 365
|
||
// 我们放大一万倍来摇号
|
||
$iProbability = (int)(($iYearlyTarget / 365) * 10000);
|
||
$iMagicNumber = mt_rand(1, 10000);
|
||
|
||
if ($iMagicNumber <= $iProbability) {
|
||
// 中奖了,加 1 个赞
|
||
$oArticle->increment('i365like');
|
||
}
|
||
}
|
||
|
||
public function like(int $iArticleId, string $sIp, string $sFingerprint): array
|
||
{
|
||
// 1. 检查是否已经点过
|
||
$bExists = ArticleLikes::where('iArticleId', $iArticleId)
|
||
->where(function ($q) use ($sIp, $sFingerprint) {
|
||
$q->where('sIp', $sIp)
|
||
->orWhere('sFingerprint', $sFingerprint);
|
||
})->exists();
|
||
|
||
if ($bExists) {
|
||
return ['status' => false, 'code' => 200, 'msg' => '感谢支持,点过赞了。'];
|
||
}
|
||
|
||
// 2. 事务扣杀
|
||
try {
|
||
// 定义一个变量来接收最新的点赞数
|
||
$iNewLikeCount = 0;
|
||
|
||
\DB::transaction(function () use ($iArticleId, $sIp, $sFingerprint, &$iNewLikeCount) {
|
||
ArticleLikes::create([
|
||
'iArticleId' => $iArticleId,
|
||
'sIp' => $sIp,
|
||
'sFingerprint' => $sFingerprint
|
||
]);
|
||
|
||
// 执行增加并直接获取最新的模型实例
|
||
$oArticle = Articles::where('id', $iArticleId);
|
||
$oArticle->increment('iLikeCount', 1, [
|
||
'sLastLikedAt' => now(), // 这里的 now() 是 Laravel 提供的 Carbon 实例
|
||
]);
|
||
$oArticle->sLastLikedAt = time();
|
||
// $oArticle->save(); // 不对吗
|
||
|
||
// 拿到自增后的最新数字
|
||
$iNewLikeCount = $oArticle->value('iLikeCount');
|
||
});
|
||
|
||
CacheService::updateOrderLikes();
|
||
|
||
return [
|
||
'status' => true,
|
||
'code' => 200,
|
||
'msg' => '感谢支持!',
|
||
'iLikeCount' => $iNewLikeCount // 药水哥绝杀:把热腾腾的数字发回去
|
||
];
|
||
} catch (\Exception $e) {
|
||
// tt($e->getMessage());
|
||
\Log::error("点赞失败: " . $e->getMessage());
|
||
return ['status' => false, 'code' => 500, 'msg' => '系统打了个盹,稍后再试。'];
|
||
}
|
||
}
|
||
|
||
public function search(string $keywords, string $resultType = 'view')
|
||
{
|
||
$term = trim(strip_tags($keywords));
|
||
abort_if($term === '' || mb_strlen($term) > 100, 404);
|
||
// Live DB authorization avoids stale external-index documents and cross-site results.
|
||
$literal = '%'.str_replace(['!','%','_'], ['!!','!%','!_'], $term).'%';
|
||
$articles = Articles::publicVisible()->with('oCategory.oParent')
|
||
->where(fn($q) => $q->whereRaw("sTitle LIKE ? ESCAPE '!'", [$literal])->orWhereRaw("sContent LIKE ? ESCAPE '!'", [$literal]))
|
||
->orderByDesc('iIsTop')->orderByDesc('created_at')->orderByDesc('id')->paginate(config('seo.page_size'));
|
||
return ['oArticles'=>$articles, 'sKeywords'=>$term];
|
||
}
|
||
public function getTagArticles(string $slug, string $resultType = 'view')
|
||
{
|
||
$tag = Tags::where('sSlug', $slug)->firstOrFail();
|
||
return ['oTag'=>$tag, 'oArticles'=>$tag->oArticles()->publicVisible()->with('oCategory.oParent')
|
||
->orderByDesc('iIsTop')->orderByDesc('created_at')->orderByDesc('id')->paginate(config('seo.page_size'))];
|
||
}
|
||
public function getLatestFreeNodeSlug(): ?string
|
||
{
|
||
foreach (Articles::publicVisible()->whereHas('oCategory', fn($q)=>$q->where('sSlug','freenode'))
|
||
->orderByDesc('created_at')->orderByDesc('id')->cursor() as $article) {
|
||
if (ResourcePublication::usable($article)) return $article->sSlug;
|
||
}
|
||
return null;
|
||
}
|
||
public function getCategoryArticles(string $slug, string $resultType = 'view')
|
||
{
|
||
$category = Categories::where('sSlug', $slug)->where('iStatus',1)->with('oParent')->firstOrFail();
|
||
abort_if($category->iParentId && (!$category->oParent || !$category->oParent->iStatus),404);
|
||
$ids = [$category->id];
|
||
if ($category->iShowChildren) $ids = array_merge($ids, Categories::where('iParentId',$category->id)->where('iStatus',1)->pluck('id')->all());
|
||
$articles = Articles::publicVisible()->whereIn('iCategoryId',$ids)->with('oCategory.oParent')
|
||
->orderByDesc('iIsTop')->orderByDesc('created_at')->orderByDesc('id')->paginate(config('seo.page_size'));
|
||
return ['oCategory'=>$category,'oArticles'=>$articles,'iArticleCount'=>$articles->total()];
|
||
}
|
||
public function getArticle(string $slug, string $ip, string $fingerprint)
|
||
{
|
||
$article = Articles::publicVisible()->where('sSlug',$slug)->with(['oCategory.oParent','oTags'])->firstOrFail();
|
||
$article->bIsLiked = ArticleLikes::where('iArticleId',$article->id)
|
||
->where(fn($q)=>$q->where('sIp',$ip)->orWhere('sFingerprint',$fingerprint))->exists();
|
||
return ['oArticle'=>$article];
|
||
}
|
||
}
|