fix(tongji): stabilize food transitions and mixed weekly rank

This commit is contained in:
大哥大哥的大哥哥
2026-07-22 15:21:51 +08:00
parent 68b5b12b59
commit 1626b6f498
12 changed files with 970 additions and 107 deletions
+95 -68
View File
@@ -8,6 +8,7 @@ use think\facade\Log;
/**
* 控糖消消乐平台能力:真实用户、每周7人同行榜、幂等成绩上报和微信分享。
* 前期榜单统一混排;sex 字段只保留为用户资料快照,不参与分组。
*/
class GamePlatformLogic
{
@@ -29,7 +30,7 @@ class GamePlatformLogic
}
/**
* 获取当前用户所在的真实周榜;首次进入会分配到当周同性别7人组。
* 获取当前用户所在的真实周榜;首次进入会分配到当周混合7人组。
*/
public static function leaderboard(int $userId, string $sessionKey = ''): array|false
{
@@ -258,27 +259,27 @@ class GamePlatformLogic
$existing['nickname'] = $profile['nickname'];
$existing['avatar'] = $profile['avatar'];
}
// 已经进入本周旧“男女榜”的用户也在下次访问时平滑迁入混合榜,
// 保留原认糖数、最高分和局数,不要求等到下周重新开始。
$oldGroupId = (int) $existing['group_id'];
$oldGroupSex = (int) Db::name('tcm_game_weekly_group')
->where('id', $oldGroupId)
->value('sex');
if ($oldGroupSex !== 0) {
$mixedGroup = self::lockAvailableMixedGroup($weekStart, $now);
Db::name('tcm_game_weekly_score')->where('id', (int) $existing['id'])->update([
'group_id' => (int) $mixedGroup['id'],
'update_time'=> $now,
]);
$existing['group_id'] = (int) $mixedGroup['id'];
self::refreshGroupMemberCount($oldGroupId, $now);
self::refreshGroupMemberCount((int) $mixedGroup['id'], $now);
}
return $existing;
}
// 每周、每个性别使用一行分配锁串行化首次入组。直接 upsert 锁行,
// 避免空分组上的间隙锁导致首批并发请求互相等待或偶发死锁。
Db::name('tcm_game_weekly_allocator')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => $profile['sex'],
'create_time'=> $now,
'update_time'=> $now,
]);
$allocator = Db::name('tcm_game_weekly_allocator')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->lock(true)
->find();
if (!$allocator) {
throw new \RuntimeException('同行分配锁创建失败');
}
$group = self::lockAvailableMixedGroup($weekStart, $now);
// 等待分配锁期间,同一用户的另一个请求可能已经完成分配。
$existing = Db::name('tcm_game_weekly_score')
@@ -290,43 +291,6 @@ class GamePlatformLogic
return $existing;
}
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->where('member_count', '<', self::GROUP_SIZE)
->order('group_no', 'asc')
->lock(true)
->find();
if (!$group) {
$maxGroupNo = (int) Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->max('group_no');
$groupNo = $maxGroupNo + 1;
// 首批用户并发进入时可能同时算出相同 group_no。利用唯一键 upsert
// 让请求汇合到同一组,再锁定该组继续分配,避免偶发 1062/死锁。
Db::name('tcm_game_weekly_group')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => $profile['sex'],
'group_no' => $groupNo,
'member_count'=> 0,
'create_time' => $now,
'update_time' => $now,
]);
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', $profile['sex'])
->where('group_no', $groupNo)
->lock(true)
->find();
if (!$group) {
throw new \RuntimeException('同行分组创建失败');
}
}
$scoreRow = [
'group_id' => (int) $group['id'],
'week_start' => $weekStart,
@@ -358,18 +322,84 @@ class GamePlatformLogic
// 仅新插入时刷新人数;使用实际成绩行数纠正历史并发造成的计数漂移。
if ($inserted === 1) {
$memberCount = (int) Db::name('tcm_game_weekly_score')
->where('group_id', (int) $group['id'])
->count();
Db::name('tcm_game_weekly_group')->where('id', (int) $group['id'])->update([
'member_count' => min(self::GROUP_SIZE, $memberCount),
'update_time' => $now,
]);
self::refreshGroupMemberCount((int) $group['id'], $now);
}
return $score;
}
/** 锁定一个可加入的混合7人组;调用方需处在数据库事务中。 */
private static function lockAvailableMixedGroup(string $weekStart, int $now): array
{
// sex=0 是当前统一混合榜。分配锁避免首批并发请求重复建组或超过7人。
Db::name('tcm_game_weekly_allocator')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => 0,
'create_time'=> $now,
'update_time'=> $now,
]);
$allocator = Db::name('tcm_game_weekly_allocator')
->where('week_start', $weekStart)
->where('sex', 0)
->lock(true)
->find();
if (!$allocator) {
throw new \RuntimeException('同行分配锁创建失败');
}
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', 0)
->where('member_count', '<', self::GROUP_SIZE)
->order('group_no', 'asc')
->lock(true)
->find();
if ($group) {
return $group;
}
$groupNo = (int) Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', 0)
->max('group_no') + 1;
Db::name('tcm_game_weekly_group')->duplicate([
'update_time',
])->insert([
'week_start' => $weekStart,
'sex' => 0,
'group_no' => $groupNo,
'member_count'=> 0,
'create_time' => $now,
'update_time' => $now,
]);
$group = Db::name('tcm_game_weekly_group')
->where('week_start', $weekStart)
->where('sex', 0)
->where('group_no', $groupNo)
->lock(true)
->find();
if (!$group) {
throw new \RuntimeException('同行分组创建失败');
}
return $group;
}
private static function refreshGroupMemberCount(int $groupId, int $now): void
{
if ($groupId <= 0) {
return;
}
$memberCount = (int) Db::name('tcm_game_weekly_score')
->where('group_id', $groupId)
->count();
Db::name('tcm_game_weekly_group')->where('id', $groupId)->update([
'member_count' => min(self::GROUP_SIZE, $memberCount),
'update_time' => $now,
]);
}
private static function buildLeaderboard(
int $groupId,
int $userId,
@@ -414,14 +444,11 @@ class GamePlatformLogic
$distance = $myIndex > 0
? max(1, (int) $players[$myIndex - 1]['count'] - (int) $me['count'] + 1)
: 0;
$group = Db::name('tcm_game_weekly_group')->where('id', $groupId)->find();
$sex = (int) ($group['sex'] ?? 0);
return [
'week_start' => $weekStart,
'week_end' => date('Y-m-d', strtotime($weekStart . ' +6 days')),
'sex' => $sex,
'sex_label' => $sex === 1 ? '男士同行' : ($sex === 2 ? '女士同行' : '同行'),
'sex' => 0,
'sex_label' => '同行',
'group_size' => self::GROUP_SIZE,
'member_count'=> count($players),
'players' => $players,