feat : rank

This commit is contained in:
2025-04-08 17:59:24 +08:00
parent 35abf99854
commit edb295cbe2
10 changed files with 495 additions and 0 deletions

View File

@@ -73,4 +73,15 @@ class ApiRedisKey
{
return 'lock:refund:user_id:'. $userId;
}
/**
* @param int $cityId
* @param int $type
* @param int $timeKey
* @return string
*/
public static function getLeaderboardByCityIdAndType(int $cityId, int $type, int $timeKey): string
{
return 'chef:leaderboard:city_id:'.$cityId.':type:'.$type.':time_key:'.$timeKey;
}
}

View File

@@ -560,6 +560,21 @@ class RedisCache
{
return $this->getRedis($poolName)->zRank($key, $value);
}
/**
* @param $key
* @param int $start
* @param int $end
* @param bool $score
* @param string $poolName
* @return array|false|Redis
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function zRevRange($key, int $start = 0, int $end = -1, $score = false, string $poolName = RedisCode::DEFAULT_DB): false|array|Redis
{
return $this->getRedis($poolName)->zRevRange($key, $start, $end,$score);
}
// +--------------------------------------------------------------------------------------------------------------------------------------------
// | geo
// +--------------------------------------------------------------------------------------------------------------------------------------------

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Constants\Common;
class LeaderboardHistoryCode
{
/**
* 排行榜类型 1=厨师评分榜 2=厨师销量榜
*/
const int TYPE_CHEF_SCORE = 1;
const int TYPE_CHEF_SALE = 2;
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Admin;
use App\Controller\AbstractController;
use App\Extend\DateUtil;
use App\Request\Admin\LoginRequest;
use App\Service\Admin\Login\LoginService;
use Hyperf\HttpServer\Annotation\Controller;
@@ -32,4 +33,14 @@ class LoginController extends AbstractController
$service = new LoginService();
return $service->handle();
}
#[RequestMapping(path: "test", methods: "GET")]
#[Scene(scene: "test")]
public function test(LoginRequest $request)
{
$res = DateUtil::getLastWeekInfo();
$res2 = DateUtil::getThisWeekInfo();
var_dump($res);
var_dump($res2);
}
}

View File

@@ -8,6 +8,7 @@ use App\Controller\AbstractController;
use App\Middleware\Api\JwtAuthMiddleware;
use App\Service\Api\System\AliStsService;
use App\Service\Api\System\CityListService;
use App\Service\Api\System\GetLeaderboardService;
use App\Service\Api\System\MiniWxConfigService;
use App\Service\Api\System\SiteListService;
use App\Service\Api\System\SystemConfigService;
@@ -71,4 +72,12 @@ class SystemController extends AbstractController
{
return (new AliStsService)->handle();
}
#[RequestMapping(path: "leaderboard", methods: "GET")]
#[Scene(scene: 'leaderboard')]
// #[Middleware(JwtAuthMiddleware::class)]
public function getLeaderboardHistory()
{
return (new GetLeaderboardService)->handle();
}
}

219
app/Cron/Chef/RankTask.php Normal file
View File

@@ -0,0 +1,219 @@
<?php
/**
* This crontab file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Cron\Chef;
use App\Constants\Admin\UserCode;
use App\Constants\Common\LeaderboardHistoryCode;
use App\Extend\DateUtil;
use App\Lib\Log;
use App\Model\Chef;
use App\Model\ChefStatement;
use App\Model\Evaluation;
use App\Model\Kitchen;
use App\Model\LeaderboardHistory;
use Exception;
use Hyperf\Crontab\Annotation\Crontab;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
#[Crontab(rule: "0 12 * * 6", name: "RankTask", singleton: true , callback: "execute", memo: "厨师排行榜数据生成")]
class RankTask
{
/**
* @var ChefStatement
*/
#[Inject]
protected ChefStatement $chefStatementModel;
/**
* @var Evaluation
*/
#[Inject]
protected Evaluation $evaluationModel;
/**
* @var LeaderboardHistory
*/
#[Inject]
protected LeaderboardHistory $leaderboardHistoryModel;
/**
* @var Log
*/
#[Inject]
protected Log $log;
/**
* @var Chef
*/
#[Inject]
protected Chef $chefModel;
/**
* @var Kitchen
*/
#[Inject]
protected Kitchen $kitchenModel;
/**
* @var array
*/
protected array $thisWeekInfo;
/**
* @var array
*/
protected array $chefInfo;
/**
* @var array
*/
protected array $kitchenSiteList;
/**
* @var array
*/
protected array $insertData;
/**
* @var string
*/
protected string $nowDate;
/**
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function execute(): void
{
try {
$this->thisWeekInfo = DateUtil::getThisWeekInfo();
if (
empty($this->thisWeekInfo) ||
empty($this->thisWeekInfo['start_date']) ||
empty($this->thisWeekInfo['end_date']) ||
empty($this->thisWeekInfo['week_number']) ||
empty($this->thisWeekInfo['year'])
) throw new Exception(__CLASS__.'数据生成失败,获取上周信息失败');
$this->nowDate = date("Y-m-d H:i:s");
$flag = $this->leaderboardHistoryModel->where('time_key',$this->thisWeekInfo['year'].$this->thisWeekInfo['week_number'])->first();
if (!empty($flag)) throw new Exception(__CLASS__.'数据生成失败,上周数据已生成');
$chefInfo = $this->chefModel
->join('admin_user', function ($join) {
$join->on('admin_user.id', '=', 'chef.user_id')
->where('admin_user.is_del', '=', UserCode::IS_NO_DEL)
->select([
'admin_user.id',
'chef.kitchen_id',
]);
})
->get();
if ($chefInfo->isEmpty()) throw new Exception(__CLASS__.'数据生成失败,获取厨师信息失败');
$this->chefInfo = $chefInfo->toArray();
$kitchenIds = array_column($this->chefInfo, 'kitchen_id');
$this->kitchenSiteList = $this->kitchenModel->whereIn('id',$kitchenIds)->pluck('city_id','id')->toArray();
if (empty($this->kitchenSiteList)) throw new Exception(__CLASS__.'数据生成失败,获取厨房城市信息失败');
$this->buildChefEvaluationRankData();
$this->buildChefStatementRankData();
if (empty($this->insertData)) throw new Exception(__CLASS__.'数据生成失败,获取厨师排行榜数据失败');
Db::transaction(function () {
$res = (new LeaderboardHistory)->insert($this->insertData);
if (!$res) throw new Exception(__CLASS__.'数据生成失败,插入数据失败');
});
}catch (Exception $e){
$this->log->error($e->getMessage());
}
}
/**
* @return void
*/
private function buildChefEvaluationRankData(): void
{
$data = $this->chefStatementModel
->whereBetween('date',[$this->thisWeekInfo['start_date'],$this->thisWeekInfo['end_date']])
->select('chef_id', Db::raw('SUM(`sale`) as total_sale'))
->groupBy('chef_id')
->get();
if ($data->isEmpty()) $this->buildChefStatementNullData(LeaderboardHistoryCode::TYPE_CHEF_SALE);
$data = array_column($data->toArray(),'total_sale','chef_id');
foreach ($this->chefInfo as $v){
$this->insertData[] = [
'chef_id' => $v->id,
'kitchen_id' => $v->kitchen_id,
'city_id' => $this->kitchenSiteList[$v->kitchen_id] ?? 0,
'time_key' => $this->thisWeekInfo['year'].$this->thisWeekInfo['week_number'],
'board_type' => LeaderboardHistoryCode::TYPE_CHEF_SALE,
'score' => $data[$v->id] ?? 0,
'create_time' => $this->nowDate,
];
}
}
private function buildChefStatementNullData(int $type): void
{
foreach ($this->chefInfo as $v) {
$this->insertData[] = [
'chef_id' => $v->id,
'kitchen_id' => $v->kitchen_id,
'city_id' => $this->kitchenSiteList[$v->kitchen_id] ?? 0,
'time_key' => $this->thisWeekInfo['year'].$this->thisWeekInfo['week_number'],
'board_type' => $type,
'score' => 0,
'create_time' => $this->nowDate,
];
}
}
/**
* @return void
*/
private function buildChefStatementRankData(): void
{
$data = $this->evaluationModel
->whereBetween('date',[$this->thisWeekInfo['start_date'],$this->thisWeekInfo['end_date']])
->select('chef_id',Db::raw('AVG(`score`) as total_score'))
->groupBy('chef_id')
->get();
if ($data->isEmpty()) $this->buildChefStatementNullData(LeaderboardHistoryCode::TYPE_CHEF_SCORE);
$data = array_column($data->toArray(),'total_score','chef_id');
foreach ($this->chefInfo as $v){
$this->insertData[] = [
'chef_id' => $v->id,
'kitchen_id' => $v->kitchen_id,
'city_id' => $this->kitchenSiteList[$v->kitchen_id] ?? 0,
'time_key' => $this->thisWeekInfo['year'].$this->thisWeekInfo['week_number'],
'board_type' => LeaderboardHistoryCode::TYPE_CHEF_SCORE,
'score' => $data[$v->id] ?? 0,
'create_time' => $this->nowDate,
];
}
}
}

View File

@@ -2,6 +2,9 @@
namespace App\Extend;
use DateMalformedStringException;
use DateTime;
class DateUtil
{
public const MINUTE = 60; // 分
@@ -126,4 +129,63 @@ class DateUtil
return $date;
}
static function getThisWeekInfo(): array
{
// 获取当前时间
$now = new DateTime();
// 获取开始日期 (周一)
$startOfWeek = clone $now;
$startOfWeek->modify('Monday this week');
// 获取上周的结束日期 (周日)
$endOfWeek = clone $now;
$endOfWeek->modify('Sunday this week');
// 获取上周是当年的第几周
$weekNumber = $now->format('W');
$year = $now->format('o');
return [
'start_date' => $startOfWeek->format('Y-m-d'),
'end_date' => $endOfWeek->format('Y-m-d'),
'week_number' => (int)$weekNumber,
'year' => (int)$year,
];
}
/**
* @return array
* @throws DateMalformedStringException
*/
static function getLastWeekInfo(): array
{
// 获取当前时间
$now = new DateTime();
// 获取上周的时间
$lastWeek = clone $now;
$lastWeek->modify('-1 week');
// 获取上周的开始日期 (周一)
$startOfWeek = clone $lastWeek;
$startOfWeek->modify('Monday this week');
// 获取上周的结束日期 (周日)
$endOfWeek = clone $lastWeek;
$endOfWeek->modify('Sunday this week');
// 获取上周是当年的第几周
$weekNumber = $lastWeek->format('W');
$year = $lastWeek->format('o');
return [
'start_date' => $startOfWeek->format('Y-m-d'),
'end_date' => $endOfWeek->format('Y-m-d'),
'week_number' => (int)$weekNumber,
'year' => (int)$year,
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property int $board_type
* @property int $time_key
* @property int $kitchen_id
* @property int $city_id
* @property int $chef_id
* @property int $score
* @property string $create_time
*/
class LeaderboardHistory extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'leaderboard_history';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'board_type' => 'integer', 'time_key' => 'integer', 'kitchen_id' => 'integer', 'city_id' => 'integer', 'chef_id' => 'integer', 'score' => 'integer'];
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\System;
use App\Cache\Redis\Api\ApiRedisKey;
use App\Cache\Redis\RedisCache;
use App\Constants\Common\LeaderboardHistoryCode;
use App\Extend\DateUtil;
use App\Model\AdminUser;
use App\Model\LeaderboardHistory;
use App\Model\SystemCity;
use App\Service\Api\BaseService;
use App\Service\ServiceTrait\Common\OssTrait;
use Hyperf\Di\Annotation\Inject;
class GetLeaderboardService extends BaseService
{
use OssTrait;
/**
* @var SystemCity
*/
#[Inject]
protected SystemCity $systemCityModel;
/**
* @var RedisCache
*/
#[Inject]
protected RedisCache $redis;
/**
* @var LeaderboardHistory
*/
#[Inject]
protected LeaderboardHistory $leaderboardHistoryModel;
/**
* @var AdminUser
*/
#[Inject]
protected AdminUser $adminUserModel;
public function handle()
{
$cityId = (int)$this->request->input('city_id');
$type = (int)$this->request->input('type');
$cityInfo = $this->systemCityModel->find($cityId);
if (empty($cityInfo)) return $this->return->success('success',['list' => []]);
$lastInfo = DateUtil::getLastWeekInfo();
if (
empty($lastInfo) ||
empty($lastInfo['start_date']) ||
empty($lastInfo['end_date']) ||
empty($lastInfo['week_number']) ||
empty($lastInfo['year'])
) return $this->return->success('success',['list' => []]);
$timeKey = (int)($lastInfo['year'].$lastInfo['week_number']);
$redisKey = ApiRedisKey::getLeaderboardByCityIdAndType($cityId,$type,$timeKey);
if (!$this->redis->exists($redisKey)) {
$info = $this->leaderboardHistoryModel->where('board_type',$type)->where('city_id',$cityId)->where('time_key',$timeKey)->pluck('score','chef_id')->toArray();
foreach ($info as $k=>$v) {
if ($type == LeaderboardHistoryCode::TYPE_CHEF_SCORE) {
if ($v == 0) $v = 0;
if ($v < 4) {
$normalized = $v / 4;
$v = 4 + 1 - exp(-2 * $normalized);
}
$v = round($v,2);
}
$this->redis->zAdd($redisKey,$v,$k);
$this->redis->expire($redisKey,86400);
}
}
$leaderboardList = $this->redis->zRevRange($redisKey,0,-1,true);
$chefIds = array_keys($leaderboardList);
$chefList = $this->adminUserModel->whereIn('id',$chefIds)->select('id','chinese_name','avatar')->get();
if ($chefList->isEmpty()) return $this->return->success('success',['list' => []]);
$chefList = $chefList->toArray();
$avatarIds = array_column($chefList,'avatar');
$avatarList = $this->getOssObjects($avatarIds);
$res = [];
foreach ($leaderboardList as $k=>$v) {
$res[] = [
'id' => $k,
'name' => $chefList[$k]['chinese_name'],
'avatar' => $avatarList[$chefList[$k]['avatar']]['url'] ?? '',
'score' => $v,
];
}
return $this->return->success('success',['list' => $res]);
}
}

View File

@@ -8,6 +8,12 @@ account=13632877014&password=123456
client.global.set("admin_token", response.body.data.token);
%}
### admintest
GET {{host}}/admin/login/test
Content-Type: application/x-www-form-urlencoded
### test
GET {{host}}/admin/third/sts/test
Content-Type: application/x-www-form-urlencoded