fn_b/app/Services/ArticlesService.php
2026-02-12 12:36:53 +08:00

278 lines
9.7 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
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');
// 拿到自增后的最新数字
$iNewLikeCount = $oArticle->value('iLikeCount');
});
CacheService::updateOrderLikes();
return [
'status' => true,
'code' => 200,
'msg' => '感谢支持!',
'iLikeCount' => $iNewLikeCount // 药水哥绝杀:把热腾腾的数字发回去
];
} catch (\Exception $e) {
\Log::error("点赞失败: " . $e->getMessage());
return ['status' => false, 'code' => 500, 'msg' => '系统打了个盹,稍后再试。'];
}
}
public function search(string $sKeywords, string $sResultType)
{
if (empty($sKeywords)) {
return [];
}
$sKeywords = strip_tags(trim($sKeywords));
$oArticles = Articles::search($sKeywords)
->options([
'attributesToHighlight' => ['sTitle', 'sContent', 'sCategoryName'],
// 1. 把 attributesToSnippet 改成 attributesToCrop
'attributesToCrop' => ['sContent'],
// 2. 设置裁剪后的长度(单位是字符,按需调整)
'cropLength' => 100,
'highlightPreTag' => '<mark class="highlight">',
'highlightPostTag' => '</mark>',
])
->orderByDesc('iIsTop')
->paginate(config('app.page_limit'));
$oArticles->getCollection()->transform(function ($oArticle) {
$aMeta = $oArticle->scoutMetadata();
$aFormatted = $aMeta['_formatted'] ?? [];
if (!empty($aFormatted)) {
$oArticle->sTitle = $aFormatted['sTitle'] ?? $oArticle->sTitle;
$oArticle->sContent = $aFormatted['sContent'] ?? $oArticle->sContent;
if (isset($aFormatted['sCategoryName'])) {
$oArticle->sCategoryName = $aFormatted['sCategoryName'];
$oArticle->setRelation('oCategory', (object)[
'sName' => $aFormatted['sCategoryName'],
'sSlug' => $oArticle->sCategorySlug ?? ''
]);
}
}
return $oArticle;
});
if ($sResultType === "ajax") {
$sHtml = '';
foreach ($oArticles as $oArticle) {
$sHtml .= view('components.article-item', ['oArticle' => $oArticle])->render();
}
$xResult = $sHtml;
} else {
$xResult = [
'oArticles' => $oArticles,
];
}
return $xResult;
}
public function getTagArticles(string $sSlug, string $sResultType)
{
$oTags = Tags::where('sSlug', $sSlug)->firstOrFail();
$oArticles = $oTags->oArticles()
->where('iStatus', 1) // 只拿已发布的
->with(['oCategory']) // 预加载关联,拒绝 N+1
->orderByDesc('iIsTop') // 置顶优先
->orderByDesc('created_at') // 最新优先
->paginate(config('app.page_limit')); // 必须分页,否则数据多了会爆内存
if ($sResultType === "ajax") {
$sHtml = '';
foreach ($oArticles as $oArticle) {
$sHtml .= view('components.article-item', ['oArticle' => $oArticle])->render();
}
$xResult = $sHtml;
} else {
$xResult = [
'oArticles' => $oArticles,
];
}
return $xResult;
}
public function getCategoryArticles(string $sSlug, string $sResultType)
{
$oCategory = Categories::where('sSlug', $sSlug)->firstOrFail();
if ($oCategory->iShowChildren) {
$aCategoryIds = Categories::where('iParentId', $oCategory->id)
->pluck('id')
->push($oCategory->id)
->toArray();
} else {
$aCategoryIds = [$oCategory->id];
}
$oArticles = Articles::whereIn('iCategoryId', $aCategoryIds)
->with(['oUser', 'oCategory'])
->orderBy('iIsTop', 'desc')
->orderBy('created_at', 'desc')
->simplePaginate(config('app.page_limit'));
if ($sResultType === "ajax") {
$sHtml = '';
foreach ($oArticles as $oArticle) {
$sHtml .= view('components.article-item', ['oArticle' => $oArticle])->render();
}
$aResult = $sHtml;
} else {
$aResult = [
'oCategory' => $oCategory,
'oArticles' => $oArticles,
];
}
return $aResult;
}
public function getArticle(string $sArticleSlug, string $sIp, string $sFingerprint)
{
$oArticle = Articles::where('sSlug', $sArticleSlug)
->where('iStatus', 1)
->with(['oCategory.oParent', 'oTags'])
->firstOrFail();
$oArticle->bIsLiked = ArticleLikes::where('iArticleId', $oArticle->id)
->where(function($q) use ($sIp, $sFingerprint) {
$q->where('sIp', $sIp)
->orWhere('sFingerprint', $sFingerprint);
})->exists();
// return $oArticle;
return [
'oArticle' => $oArticle
];
}
}