fix(tongji): 修复小游戏平台同步与并发问题
This commit is contained in:
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
- 复用主小程序 `token` 与微信小程序登录,不创建第二套游戏账号。
|
- 复用主小程序 `token` 与微信小程序登录,不创建第二套游戏账号。
|
||||||
- 按周一日期、性别自动分配最多 7 人的同行组。
|
- 按周一日期、性别自动分配最多 7 人的同行组。
|
||||||
|
- 首次入组使用数据库分配锁,多个用户同时进入也不会重复分组或超过 7 人。
|
||||||
- 以真实平台昵称、头像、认糖数和本周最高分排序。
|
- 以真实平台昵称、头像、认糖数和本周最高分排序。
|
||||||
- 每局用 `session_key` 上报绝对进度,断网重试不会重复加分。
|
- 每局用 `session_key` 上报绝对进度,断网重试不会重复加分。
|
||||||
- 待同步成绩最多本地保留 8 局,重新联网后自动补传。
|
- 待同步成绩最多本地保留 8 局,重新联网后自动补传。
|
||||||
- 微信好友与朋友圈分享使用随机分享码,不在链接中暴露用户 ID。
|
- 微信好友与朋友圈分享使用随机分享码,不在链接中暴露用户 ID。
|
||||||
- 受邀进入只记录一次轻量分享访问,不自动建立家庭或好友绑定。
|
- 每张周分享卡对同一受邀人只记录一次轻量访问,不自动建立家庭或好友绑定。
|
||||||
|
|
||||||
## 接口
|
## 接口
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@
|
|||||||
2. 发布 `server/app/api/logic/tcm/GamePlatformLogic.php` 和 `TcmController.php`。
|
2. 发布 `server/app/api/logic/tcm/GamePlatformLogic.php` 和 `TcmController.php`。
|
||||||
3. 重新构建并上传小程序前端。
|
3. 重新构建并上传小程序前端。
|
||||||
|
|
||||||
|
如果数据库已经执行过本功能的旧版建表脚本,再执行一次
|
||||||
|
`server/sql/1.9.20260717/upgrade_tcm_endless_game_platform_20260717.sql`,用于补充分组锁并把分享去重范围修正为“每张周分享卡”。
|
||||||
|
|
||||||
如果后端或数据表尚未发布,游戏仍可离线游玩,榜单会显示“离线记录中”;联网且接口可用后自动补传。
|
如果后端或数据表尚未发布,游戏仍可离线游玩,榜单会显示“离线记录中”;联网且接口可用后自动补传。
|
||||||
|
|
||||||
## 上线前检查
|
## 上线前检查
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
|
- `index.vue`:页面、棋盘算法、关卡主题、任务、道具、三/四/五连奖励和适老化交互。
|
||||||
- `game-endless.scss`:完整页面与动画样式。
|
- `game-endless.scss`:完整页面与动画样式。
|
||||||
- `components/TongjiIcon.vue`:本功能包使用的图标组件。
|
- `components/TongjiIcon.vue`:本功能包使用的图标组件。
|
||||||
|
- `composables/useGameAuth.js`:复用主小程序账号并处理 token 过期重登。
|
||||||
|
- `composables/useGamePlatform.js`:周榜、成绩补传与微信分享的平台连接层。
|
||||||
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
|
- `composables/useGameSfx.js`:滑动、掉落、消除、连击和大奖音效。
|
||||||
- `utils/svgDataUrl.js`:图标编码工具。
|
- `utils/svgDataUrl.js`:图标编码工具。
|
||||||
- `assets/food/`:本游戏使用的全部食品与驼乳粉图片。
|
- `assets/food/`:本游戏使用的全部食品与驼乳粉图片。
|
||||||
@@ -37,7 +39,9 @@ build: {
|
|||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "控糖消消乐",
|
"navigationBarTitleText": "控糖消消乐",
|
||||||
"backgroundColor": "#eefbf4",
|
"backgroundColor": "#eefbf4",
|
||||||
"disableScroll": false
|
"disableScroll": false,
|
||||||
|
"enableShareAppMessage": true,
|
||||||
|
"enableShareTimeline": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -51,6 +55,6 @@ build: {
|
|||||||
- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。
|
- 当前项目原来的“糖分突袭”源码保存在 `tongji/legacy-game/game.vue`,没有注册为页面,不参与打包。
|
||||||
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
- 连消小目标按“连消×2”累计 2 次;连续 6 次普通消除没有连消时,下一次掉落会提供连消机会。“连消×3”保留为额外积分、驼乳粉与撒花惊喜,不阻挡主线任务。
|
||||||
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
- 横向 3 个与竖向 3 个相交形成 L/T 形五消时,会在交点生成带高对比 L 标记的范围爆破棋子;该棋子再次被消除时清除周围九格。
|
||||||
- 进入游戏时展示“本周 7 人同行榜”界面原型,包含前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。当前榜单明确标注为预览数据,正式用户关系和周榜成绩需要后端接口接入。
|
- 进入游戏时展示“本周 7 人同行榜”,包含真实平台昵称、前后名次、差距提示和可切换的同行/亲友鼓励;顶部“本周同行”可再次打开。后端部署方式见 `PLATFORM_INTEGRATION.md`。
|
||||||
- 本功能包依赖 uni-app Vue 3 和 `@dcloudio/uni-app`,不依赖当前项目其他业务组件。
|
- 本功能包依赖 uni-app Vue 3 和 `@dcloudio/uni-app`,不依赖当前项目其他业务组件。
|
||||||
- 食物风险等级和科普文案集中在 `index.vue` 的 `FOODS` 配置中,正式上线前仍需由医院医生审核。
|
- 食物风险等级和科普文案集中在 `index.vue` 的 `FOODS` 配置中,正式上线前仍需由医院医生审核。
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 控糖消消乐独立登录适配层。复用主小程序账号,但不依赖 tongji 其他页面代码。
|
||||||
|
*/
|
||||||
|
export function useGameAuth(proxy) {
|
||||||
|
let loginPromise = null
|
||||||
|
|
||||||
|
function hasToken() {
|
||||||
|
return !!String(uni.getStorageSync('token') || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
function wxLoginCode() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (res) => res?.code ? resolve(res.code) : reject(new Error('微信登录未返回 code')),
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithCode(code) {
|
||||||
|
const res = await proxy.apiUrl({
|
||||||
|
url: '/api/login/mnpLogin',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code }
|
||||||
|
}, false)
|
||||||
|
if (res?.code !== 1 || !res.data?.token) {
|
||||||
|
clearToken()
|
||||||
|
throw new Error(res?.msg || '登录失败')
|
||||||
|
}
|
||||||
|
uni.setStorageSync('token', res.data.token)
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyOrLogin() {
|
||||||
|
if (hasToken()) {
|
||||||
|
try {
|
||||||
|
const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
|
||||||
|
if (res?.code === 1 && res.data) {
|
||||||
|
uni.setStorageSync('userData', res.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 只有明确的登录失效才重新换取 token;其他业务错误先保留原登录。
|
||||||
|
if (res?.code !== -1) return true
|
||||||
|
} catch (_) {
|
||||||
|
// 断网不清除仍可能有效的 token,成绩由离线队列稍后补传。
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
clearToken()
|
||||||
|
}
|
||||||
|
const code = await wxLoginCode()
|
||||||
|
return loginWithCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureLoggedIn() {
|
||||||
|
if (loginPromise) return loginPromise
|
||||||
|
loginPromise = verifyOrLogin()
|
||||||
|
.then(ok => !!ok)
|
||||||
|
.catch(() => false)
|
||||||
|
// 只合并同时发生的登录;成功结果不能永久缓存,否则 token 过期后无法恢复。
|
||||||
|
.finally(() => { loginPromise = null })
|
||||||
|
return loginPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ensureLoggedIn }
|
||||||
|
}
|
||||||
@@ -182,7 +182,14 @@ export function useGamePlatform(proxy, ensureLoggedIn) {
|
|||||||
throw new Error(res?.msg || '成绩保存失败')
|
throw new Error(res?.msg || '成绩保存失败')
|
||||||
}
|
}
|
||||||
applyLeaderboard(res.data, payload.session_key)
|
applyLeaderboard(res.data, payload.session_key)
|
||||||
queuedPayloads = queuedPayloads.filter(item => item.session_key !== payload.session_key)
|
// 请求发出后玩家可能又完成了消除。只移除已经被本次请求覆盖的进度,
|
||||||
|
// 不能按 session_key 整局删除,否则会丢失请求进行期间产生的新进度。
|
||||||
|
queuedPayloads = queuedPayloads.filter(item => (
|
||||||
|
item.session_key !== payload.session_key
|
||||||
|
|| Number(item.learned_count) > Number(payload.learned_count)
|
||||||
|
|| Number(item.score) > Number(payload.score)
|
||||||
|
|| Number(item.ended) > Number(payload.ended)
|
||||||
|
))
|
||||||
persistPendingPayloads()
|
persistPendingPayloads()
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
connected.value = false
|
connected.value = false
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ import { onLoad, onShareAppMessage, onShareTimeline, onShow, onUnload } from '@d
|
|||||||
import TongjiIcon from './components/TongjiIcon.vue'
|
import TongjiIcon from './components/TongjiIcon.vue'
|
||||||
import { useGameSfx } from './composables/useGameSfx.js'
|
import { useGameSfx } from './composables/useGameSfx.js'
|
||||||
import { useGamePlatform } from './composables/useGamePlatform.js'
|
import { useGamePlatform } from './composables/useGamePlatform.js'
|
||||||
import { useTongjiAuth } from '../composables/useTongjiAuth.js'
|
import { useGameAuth } from './composables/useGameAuth.js'
|
||||||
|
|
||||||
const FOOD_IMAGE_MODULES = import.meta.glob('./assets/food/*.jpg', {
|
const FOOD_IMAGE_MODULES = import.meta.glob('./assets/food/*.jpg', {
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -398,7 +398,7 @@ const {
|
|||||||
} = useGameSfx()
|
} = useGameSfx()
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance()
|
const { proxy } = getCurrentInstance()
|
||||||
const { ensureLoggedIn } = useTongjiAuth(proxy)
|
const { ensureLoggedIn } = useGameAuth(proxy)
|
||||||
const {
|
const {
|
||||||
connected: platformConnected,
|
connected: platformConnected,
|
||||||
loading: platformLoading,
|
loading: platformLoading,
|
||||||
@@ -953,7 +953,11 @@ async function resolveMatches(initialGroups, movedIndex) {
|
|||||||
const camelMatchCount = Math.max(bentCamelCount, camelGroup?.indices.length || 0)
|
const camelMatchCount = Math.max(bentCamelCount, camelGroup?.indices.length || 0)
|
||||||
if (camelMatchCount >= 3) applyCamelJackpot(camelMatchCount, remove)
|
if (camelMatchCount >= 3) applyCamelJackpot(camelMatchCount, remove)
|
||||||
const removedTiles = [...remove].map(i => board.value[i]).filter(Boolean)
|
const removedTiles = [...remove].map(i => board.value[i]).filter(Boolean)
|
||||||
const removedNormal = removedTiles.filter(tile => !foodOf(tile.key).high)
|
// 排名统计的是正确认识并消除的普通食物;驼乳是道具,不计入认糖数。
|
||||||
|
const removedNormal = removedTiles.filter(tile => {
|
||||||
|
const food = foodOf(tile.key)
|
||||||
|
return !food.high && !food.product
|
||||||
|
})
|
||||||
const fireRemovedHigh = fireClearingIndices.value.some(index => foodOf(board.value[index]?.key).high)
|
const fireRemovedHigh = fireClearingIndices.value.some(index => foodOf(board.value[index]?.key).high)
|
||||||
const largestMatch = Math.max(
|
const largestMatch = Math.max(
|
||||||
...groups.map(group => group.indices.length),
|
...groups.map(group => group.indices.length),
|
||||||
|
|||||||
@@ -195,18 +195,16 @@ class GamePlatformLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
$exists = Db::name('tcm_game_share_visit')
|
$inserted = Db::name('tcm_game_share_visit')->duplicate([
|
||||||
->where('inviter_user_id', $inviterUserId)
|
'invite_code',
|
||||||
->where('visitor_user_id', $userId)
|
])->insert([
|
||||||
->lock(true)
|
'invite_code' => $inviteCode,
|
||||||
->find();
|
'inviter_user_id' => $inviterUserId,
|
||||||
if (!$exists) {
|
'visitor_user_id' => $userId,
|
||||||
Db::name('tcm_game_share_visit')->insert([
|
'create_time' => time(),
|
||||||
'invite_code' => $inviteCode,
|
]);
|
||||||
'inviter_user_id' => $inviterUserId,
|
$accepted = $inserted === 1;
|
||||||
'visitor_user_id' => $userId,
|
if ($accepted) {
|
||||||
'create_time' => time(),
|
|
||||||
]);
|
|
||||||
Db::name('tcm_game_share_invite')->where('id', (int) $invite['id'])->inc('open_count')->update([
|
Db::name('tcm_game_share_invite')->where('id', (int) $invite['id'])->inc('open_count')->update([
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
]);
|
]);
|
||||||
@@ -215,7 +213,7 @@ class GamePlatformLogic
|
|||||||
|
|
||||||
$inviter = Db::name('user')->where('id', $inviterUserId)->field('nickname')->find();
|
$inviter = Db::name('user')->where('id', $inviterUserId)->field('nickname')->find();
|
||||||
return [
|
return [
|
||||||
'accepted' => !$exists,
|
'accepted' => $accepted,
|
||||||
'message' => '已加入控糖消消乐',
|
'message' => '已加入控糖消消乐',
|
||||||
'inviter' => self::displayName((string) ($inviter['nickname'] ?? ''), $inviterUserId),
|
'inviter' => self::displayName((string) ($inviter['nickname'] ?? ''), $inviterUserId),
|
||||||
];
|
];
|
||||||
@@ -228,15 +226,18 @@ class GamePlatformLogic
|
|||||||
|
|
||||||
private static function ensureWeeklyScore(int $userId, string $weekStart): array
|
private static function ensureWeeklyScore(int $userId, string $weekStart): array
|
||||||
{
|
{
|
||||||
|
$profile = self::userProfile($userId);
|
||||||
|
$now = time();
|
||||||
$existing = Db::name('tcm_game_weekly_score')
|
$existing = Db::name('tcm_game_weekly_score')
|
||||||
->where('week_start', $weekStart)
|
->where('week_start', $weekStart)
|
||||||
->where('user_id', $userId)
|
->where('user_id', $userId)
|
||||||
->lock(true)
|
|
||||||
->find();
|
->find();
|
||||||
$profile = self::userProfile($userId);
|
|
||||||
$now = time();
|
|
||||||
|
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
|
$existing = Db::name('tcm_game_weekly_score')
|
||||||
|
->where('id', (int) $existing['id'])
|
||||||
|
->lock(true)
|
||||||
|
->find();
|
||||||
$profileChanged = (string) $existing['nickname'] !== $profile['nickname']
|
$profileChanged = (string) $existing['nickname'] !== $profile['nickname']
|
||||||
|| (string) $existing['avatar'] !== $profile['avatar'];
|
|| (string) $existing['avatar'] !== $profile['avatar'];
|
||||||
if ($profileChanged) {
|
if ($profileChanged) {
|
||||||
@@ -251,6 +252,35 @@ class GamePlatformLogic
|
|||||||
return $existing;
|
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('同行分配锁创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待分配锁期间,同一用户的另一个请求可能已经完成分配。
|
||||||
|
$existing = Db::name('tcm_game_weekly_score')
|
||||||
|
->where('week_start', $weekStart)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->lock(true)
|
||||||
|
->find();
|
||||||
|
if ($existing) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
$group = Db::name('tcm_game_weekly_group')
|
$group = Db::name('tcm_game_weekly_group')
|
||||||
->where('week_start', $weekStart)
|
->where('week_start', $weekStart)
|
||||||
->where('sex', $profile['sex'])
|
->where('sex', $profile['sex'])
|
||||||
@@ -265,7 +295,11 @@ class GamePlatformLogic
|
|||||||
->where('sex', $profile['sex'])
|
->where('sex', $profile['sex'])
|
||||||
->max('group_no');
|
->max('group_no');
|
||||||
$groupNo = $maxGroupNo + 1;
|
$groupNo = $maxGroupNo + 1;
|
||||||
$groupId = Db::name('tcm_game_weekly_group')->insertGetId([
|
// 首批用户并发进入时可能同时算出相同 group_no。利用唯一键 upsert,
|
||||||
|
// 让请求汇合到同一组,再锁定该组继续分配,避免偶发 1062/死锁。
|
||||||
|
Db::name('tcm_game_weekly_group')->duplicate([
|
||||||
|
'update_time',
|
||||||
|
])->insert([
|
||||||
'week_start' => $weekStart,
|
'week_start' => $weekStart,
|
||||||
'sex' => $profile['sex'],
|
'sex' => $profile['sex'],
|
||||||
'group_no' => $groupNo,
|
'group_no' => $groupNo,
|
||||||
@@ -273,10 +307,18 @@ class GamePlatformLogic
|
|||||||
'create_time' => $now,
|
'create_time' => $now,
|
||||||
'update_time' => $now,
|
'update_time' => $now,
|
||||||
]);
|
]);
|
||||||
$group = ['id' => $groupId, 'member_count' => 0];
|
$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('同行分组创建失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scoreId = Db::name('tcm_game_weekly_score')->insertGetId([
|
$scoreRow = [
|
||||||
'group_id' => (int) $group['id'],
|
'group_id' => (int) $group['id'],
|
||||||
'week_start' => $weekStart,
|
'week_start' => $weekStart,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
@@ -289,25 +331,34 @@ class GamePlatformLogic
|
|||||||
'sex' => $profile['sex'],
|
'sex' => $profile['sex'],
|
||||||
'create_time' => $now,
|
'create_time' => $now,
|
||||||
'update_time' => $now,
|
'update_time' => $now,
|
||||||
]);
|
|
||||||
Db::name('tcm_game_weekly_group')->where('id', (int) $group['id'])->update([
|
|
||||||
'member_count' => min(self::GROUP_SIZE, (int) $group['member_count'] + 1),
|
|
||||||
'update_time' => $now,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => $scoreId,
|
|
||||||
'group_id' => (int) $group['id'],
|
|
||||||
'week_start' => $weekStart,
|
|
||||||
'user_id' => $userId,
|
|
||||||
'learned_count' => 0,
|
|
||||||
'best_score' => 0,
|
|
||||||
'games_played' => 0,
|
|
||||||
'share_count' => 0,
|
|
||||||
'nickname' => $profile['nickname'],
|
|
||||||
'avatar' => $profile['avatar'],
|
|
||||||
'sex' => $profile['sex'],
|
|
||||||
];
|
];
|
||||||
|
$inserted = Db::name('tcm_game_weekly_score')->duplicate([
|
||||||
|
'nickname',
|
||||||
|
'avatar',
|
||||||
|
'sex',
|
||||||
|
'update_time',
|
||||||
|
])->insert($scoreRow);
|
||||||
|
$score = Db::name('tcm_game_weekly_score')
|
||||||
|
->where('week_start', $weekStart)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->lock(true)
|
||||||
|
->find();
|
||||||
|
if (!$score) {
|
||||||
|
throw new \RuntimeException('同行成绩创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅新插入时刷新人数;使用实际成绩行数纠正历史并发造成的计数漂移。
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function buildLeaderboard(
|
private static function buildLeaderboard(
|
||||||
@@ -435,11 +486,12 @@ class GamePlatformLogic
|
|||||||
|
|
||||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
for ($attempt = 0; $attempt < 5; $attempt++) {
|
||||||
$code = strtoupper(bin2hex(random_bytes(6)));
|
$code = strtoupper(bin2hex(random_bytes(6)));
|
||||||
if (Db::name('tcm_game_share_invite')->where('invite_code', $code)->find()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$now = time();
|
$now = time();
|
||||||
Db::name('tcm_game_share_invite')->insert([
|
// 同一用户并发打开榜单时,以 week_start + user_id 唯一键汇合;
|
||||||
|
// 极小概率随机码撞车时,查询不到本人的记录就继续生成新码。
|
||||||
|
Db::name('tcm_game_share_invite')->duplicate([
|
||||||
|
'update_time',
|
||||||
|
])->insert([
|
||||||
'invite_code' => $code,
|
'invite_code' => $code,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'week_start' => $weekStart,
|
'week_start' => $weekStart,
|
||||||
@@ -447,7 +499,13 @@ class GamePlatformLogic
|
|||||||
'create_time' => $now,
|
'create_time' => $now,
|
||||||
'update_time' => $now,
|
'update_time' => $now,
|
||||||
]);
|
]);
|
||||||
return $code;
|
$invite = Db::name('tcm_game_share_invite')
|
||||||
|
->where('week_start', $weekStart)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->find();
|
||||||
|
if ($invite) {
|
||||||
|
return (string) $invite['invite_code'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw new \RuntimeException('分享码生成失败');
|
throw new \RuntimeException('分享码生成失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
-- 控糖消消乐:真实用户周榜、幂等局记录与轻量分享访问关系
|
-- 控糖消消乐:真实用户周榜、幂等局记录与轻量分享访问关系
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_allocator` (
|
||||||
|
`week_start` date NOT NULL COMMENT '周一日期',
|
||||||
|
`sex` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '同行组性别:0未知 1男 2女',
|
||||||
|
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||||
|
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`week_start`, `sex`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐每周同行分组并发锁';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_group` (
|
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_group` (
|
||||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`week_start` date NOT NULL COMMENT '周一日期',
|
`week_start` date NOT NULL COMMENT '周一日期',
|
||||||
@@ -68,6 +76,7 @@ CREATE TABLE IF NOT EXISTS `zyt_tcm_game_share_visit` (
|
|||||||
`visitor_user_id` int unsigned NOT NULL COMMENT '受邀打开用户ID',
|
`visitor_user_id` int unsigned NOT NULL COMMENT '受邀打开用户ID',
|
||||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_inviter_visitor` (`inviter_user_id`, `visitor_user_id`),
|
UNIQUE KEY `uk_invite_visitor` (`invite_code`, `visitor_user_id`),
|
||||||
|
KEY `idx_inviter` (`inviter_user_id`),
|
||||||
KEY `idx_visitor` (`visitor_user_id`)
|
KEY `idx_visitor` (`visitor_user_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐分享打开记录(不做强绑定)';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐分享打开记录(不做强绑定)';
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
-- 仅用于已经执行过旧版小游戏建表脚本的数据库。
|
||||||
|
-- 新安装直接执行 add_tcm_endless_game_platform.sql 即可。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `zyt_tcm_game_weekly_allocator` (
|
||||||
|
`week_start` date NOT NULL COMMENT '周一日期',
|
||||||
|
`sex` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '同行组性别:0未知 1男 2女',
|
||||||
|
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||||
|
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`week_start`, `sex`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='控糖消消乐每周同行分组并发锁';
|
||||||
|
|
||||||
|
SET @has_old_visit_index = (
|
||||||
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'zyt_tcm_game_share_visit'
|
||||||
|
AND index_name = 'uk_inviter_visitor'
|
||||||
|
);
|
||||||
|
SET @drop_old_visit_index_sql = IF(
|
||||||
|
@has_old_visit_index > 0,
|
||||||
|
'ALTER TABLE `zyt_tcm_game_share_visit` DROP INDEX `uk_inviter_visitor`',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE drop_old_visit_index_stmt FROM @drop_old_visit_index_sql;
|
||||||
|
EXECUTE drop_old_visit_index_stmt;
|
||||||
|
DEALLOCATE PREPARE drop_old_visit_index_stmt;
|
||||||
|
|
||||||
|
SET @has_invite_visit_index = (
|
||||||
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'zyt_tcm_game_share_visit'
|
||||||
|
AND index_name = 'uk_invite_visitor'
|
||||||
|
);
|
||||||
|
SET @add_invite_visit_index_sql = IF(
|
||||||
|
@has_invite_visit_index = 0,
|
||||||
|
'ALTER TABLE `zyt_tcm_game_share_visit` ADD UNIQUE INDEX `uk_invite_visitor` (`invite_code`, `visitor_user_id`)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE add_invite_visit_index_stmt FROM @add_invite_visit_index_sql;
|
||||||
|
EXECUTE add_invite_visit_index_stmt;
|
||||||
|
DEALLOCATE PREPARE add_invite_visit_index_stmt;
|
||||||
|
|
||||||
|
SET @has_inviter_index = (
|
||||||
|
SELECT COUNT(*) FROM information_schema.statistics
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'zyt_tcm_game_share_visit'
|
||||||
|
AND index_name = 'idx_inviter'
|
||||||
|
);
|
||||||
|
SET @add_inviter_index_sql = IF(
|
||||||
|
@has_inviter_index = 0,
|
||||||
|
'ALTER TABLE `zyt_tcm_game_share_visit` ADD INDEX `idx_inviter` (`inviter_user_id`)',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE add_inviter_index_stmt FROM @add_inviter_index_sql;
|
||||||
|
EXECUTE add_inviter_index_stmt;
|
||||||
|
DEALLOCATE PREPARE add_inviter_index_stmt;
|
||||||
Reference in New Issue
Block a user