fix(tongji): stabilize food transitions and mixed weekly rank
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user