基础账号与私人同步API v0.8.1#
2026-09-13,设计态。基路径 /api/v1,HTTPS 同源 JSON,字段使用 snake_case。以下新增上限与时间均 [待测试];标为协议约束的幂等和隔离规则不作为调参项。
本文件仅定义账号与私人同步;正式每日、榜单、受限回放走多人API。个人录像正文、元数据、录制草稿与删除事件只存在本机,不使用任何云接口。官方Top10候选的压缩过程证据使用独立上传授权,不混入私人同步。离线升级与多端对账见经验与防作弊。
1. 公共契约#
类型约定:ID 为小写标准 UUID;整数须在 ±9,007,199,254,740,991 内,经验单位为百分之一经验。时间为 UTC 毫秒,日期为真实 Gregorian UTC YYYY-MM-DD(不仅检查正则)。revision 为服务端发布的不可变字符串;摘要为小写 SHA-256 hex。请求对象拒绝未知字段、重复 JSON key、NaN、Infinity;可空和可省略区分,本文 ? 表示可省略。昵称先 Unicode NFC,再按码点计数;密码不归一化。
身份 G/R:G 为活跃游客或注册会话,R 仅注册用户,P 为公共入口。所有已认证路由都查询 SQLite sessions + users,检查吊销、空闲/绝对到期、auth_version、用户状态。鉴权 SQL 必须在权限敏感写事务内再次确认,避免并发退出后旧请求仍提交。返回 401/403 后本地队列原样保留。
请求头:Content-Type: application/json(文件接口例外);客户端传 X-Client-Version、X-Protocol-Version: 1。G/R 请求还传 X-Expected-User-Id,与会话不符返回 409 ACCOUNT_CHANGED;该头不能授予身份。写接口传 X-CSRF-Token 并验证精确 Origin;服务端生成 request_id。所有私有响应 Cache-Control: no-store,禁止反向代理缓存。
成功包络(204、二进制下载除外):
{"data":{"user_id":"10000000-0000-4000-8000-000000000001"},"meta":{"request_id":"req-example","server_time":1789171200000,"protocol_version":1}}失败包络:
{"error":{"code":"VERSION_CONFLICT","message":"另一台设备已更新这份存档。","retryable":false,"details":{"current_version":8}},"meta":{"request_id":"req-example","server_time":1789171200000,"protocol_version":1}}| HTTP | 错误码 | 客户端行为 |
|---|---|---|
| 400 / 422 | INVALID_REQUEST / INVALID_STATE / UNSUPPORTED_REVISION | 隔离错误操作并保留本地数据;未知版本提示更新 |
| 401 | AUTH_REQUIRED / SESSION_EXPIRED / INVALID_CREDENTIALS | 停止同步,允许离线继续;登录失败不泄露账号存在性 |
| 403 | CSRF_INVALID / ACCOUNT_DISABLED / REAUTH_REQUIRED | 刷新 CSRF/重新验证;不清空存档 |
| 404 | NOT_FOUND | 未找到或非本人对象使用相同返回 |
| 409 | ACCOUNT_CHANGED / VERSION_CONFLICT / RUN_ALREADY_SETTLED / ENTITY_RETIRED / IDEMPOTENCY_MISMATCH / PREVIEW_STALE / ACCOUNT_TAKEN | 分别暂停旧账号、保留分支、去重或重新预览,不能盲重试 |
| 409 | DEPENDENCY_PENDING / DAILY_NOT_YET_AVAILABLE | 等待前置操作/服务器日期,使用原 op 重试;不记最终回执 |
| 410 | RESET_REQUIRED / ARTIFACT_EXPIRED / RETRY_PROOF_EXPIRED | 重建同步基线或重新开始对应流程 |
| 413 | PAYLOAD_TOO_LARGE / QUOTA_EXCEEDED | 保留本地,拆包或由玩家整理 |
| 429 | RATE_LIMITED | 遵循整数秒 Retry-After,随后随机抖动 |
| 503 | SERVICE_UNAVAILABLE / WRITE_BUSY / VALIDATION_BUSY | 指数退避,游戏不停止;不得假设失败必然代表未提交 |
读列表默认 50、最大 200 条,按不可变排序键分页,私有游标签名绑定玩家及筛选条件。sync 特殊分页见后文。
在线比赛存在时,退出/改密/删除必须同步撤WS授权并经多人API的离场协调流程;不能直接删账号/席位破坏冻结榜引用。公开录像先撤除,已冻结行保留最小脱敏引用壳,见多人架构的删除规则。下文会话与数据删除步骤在该协调之后执行。
2. CSRF、会话及认证幂等#
2.1 初始化#
GET /auth/session(P/G):未登录返回 {authenticated:false, csrf_token},设置短期签名 __Host-ms_pre_auth Cookie;已登录返回 {authenticated:true, user:{id,kind,display_name}, device_id, csrf_token, idle_expires_at, absolute_expires_at}。
预认证 CSRF 使用签名 Cookie 的随机 nonce 派生;登录后使用 HMAC(csrf_key, session_token) 派生,并核对 sessions.csrf_hash,GET 不轮换当前会话的 CSRF,避免多标签页互相失效。两种 Cookie 均 HttpOnly、Secure、SameSite=Lax、Path=/,不设置 Domain。严格同源校验覆盖匿名初始化、登录和恢复;客户端从 JSON 读取 CSRF 置于内存。Cookie token 不出现在 JSON。
会话 Cookie 名 __Host-ms_session,256 位随机 token,SQLite 只保存 SHA-256。空闲 7 天、注册绝对 30 天、游客绝对 90 天为初值;按服务器时间判断,活跃会话 last_seen/空闲续期最多每 60 秒落一次盘,不能每个 GET 写库。
2.2 认证操作的短期重试证明#
创建/轮换凭据的接口(guest/register/login/password change/recover/recovery rotate)要求 Idempotency-Key: UUID 与 X-Retry-Proof: 32字节随机数的base64url。同一尝试先在本机短期安全上下文保存 proof,重试使用完全相同的请求。proof 是短期敏感恢复能力,不是设备 ID;禁止写日志,Web 首期只保留当前页面内存,刷新后按正常会话或账号登录恢复。
服务器用独立 HMAC 密钥计算请求 MAC,不能持久化密码的裸 SHA-256;原结果和需恢复的 Cookie/恢复码以外部密钥 AEAD 加密存入 auth_receipts,TTL 10 分钟(覆盖常见网络重试,待测)。AEAD 附加数据绑定 scope、request_id、proof_hash、user_id。创建身份/会话及 receipt 在同一事务提交;密码哈希提前算好,事务内重新核对旧密码凭据版本。
重复调用先做 Origin、CSRF、proof 校验;同 scope/key 不同 MAC 返回 409。只有 receipt 引用的玩家仍有效、原结果会话未撤销且 auth_version 未变,才重发原 Cookie 与加密结果;不能让重试复活被撤销的会话。对 rotate 恢复码还检查新码未被消费且未被下一次轮换取代。已过期 receipt 不恢复秘密。尚未改变凭据的普通写操作按版本或各自业务键幂等。
注册成功后可用重试 proof 取回一次生成结果,避免“密码身份已建但恢复码丢失”;receipt 的短期重复展示只针对同一次操作,不是永久查询恢复码接口。退出、改密或用户删除时同步清除相关敏感 receipt。
3. 账号接口#
以下 body 为闭合对象。device={client_install_id:UUID,label:string(1..80),platform:"web"};安装 ID 只识别设备,不认证。
| 路由 / 权限 | 请求 body | data / 行为 |
|---|---|---|
POST /auth/guest P | {device} | 201 {user,device_id},设置 Cookie;创建 users/devices/session/summary/sync_state。已有会话返回 409 SESSION_PRESENT,避免误覆盖 |
POST /auth/register G(guest) | {account,password,display_name?} | 200 {user,recovery_code},原 user 升级,撤销旧游客会话、签发新会话;已注册的非同一次重试返回 409 |
POST /auth/login P | {account,password,device} | 200 {user,device_id}、Cookie;已有 Cookie 时只撤销当前旧会话,不撤销旧账号的其他设备 |
POST /auth/logout G | {} | 204,撤销当前会话,清 Cookie;已撤销/过期重试仍 204,仍验证 Origin |
POST /auth/logout-all R | {current_password} | 204,auth_version +1,撤销全部会话、清 Cookie;不自动重登 |
POST /auth/password/change R | {current_password,new_password} | 200 {user},全部旧会话失效,新会话沿用当前设备;恢复码继续有效 |
POST /auth/password/recover P | {account,recovery_code,new_password,device} | 200 {user,recovery_code},核销旧码、撤销全部会话、签发新会话及新码;失败统一凭据错误 |
POST /auth/recovery-codes/rotate R | {current_password} | 200 {recovery_code},旧未用码全部失效,仅允许一次操作结果短期重取 |
GET /me G | 无 | {user:{id,kind,display_name,profile_version},summary,settings,quota};summary 带 version |
PATCH /me G | {base_version,display_name} | 新 user profile;base_version 对应 profile_version,CAS 更新+同步变更 |
GET /me/sessions R | 无 | {items:[{id,device_label,platform,last_seen_at,is_current}],next_cursor};不返回 token/hash |
DELETE /me/sessions/:id R | 无 | 204,只能撤销本账号会话;撤销当前时清 Cookie;删除已撤销对象幂等 |
账号规范化为小写 ASCII,^[a-z][a-z0-9_]{3,31}$;昵称 1–32 码点,拒绝控制字符;密码 12–128 码点且 ≤512 UTF-8 字节,不 trim。以上可用性阈值待测,改动须同步 DDL/API。恢复码为 32 个随机字节 base64url(43 字符),不采用易猜问题。注册不要求邮箱;无恢复码、无有效会话、无其他身份时,不能凭昵称恢复。
4. 游客合并、导出与删除#
| 路由 / 权限 | 请求 | 响应与约束 |
|---|---|---|
POST /me/merge-ticket G(guest) | {} | 201 {ticket,expires_at},一次性 256 位随机秘密,TTL 10 分钟;仅存 hash |
POST /me/merge-preview R | {ticket} | {merge_id,source_revision,target_revision,expires_at,preview};票据首次预览绑定目标,别的目标不可复用;同目标/票据返回或刷新同一预览 |
POST /me/merge R | {merge_id,source_revision,target_revision} | {merge_id,result,bootstrap_required:true};已完成同 ID 返回原结果;预览变化返回 PREVIEW_STALE |
POST /me/export G | {},Idempotency-Key | 202 {job_id};相同 key 同玩家返回原任务;业务去重用 background_jobs.dedupe_key |
GET /me/exports/:job_id G | 无 | `{state:"pending |
GET /me/exports/:job_id/pages/:index G | 无 | 完成后按 manifest 下载私有页;仅包括可迁移私人档案,无个人录像/录制草稿、多人内部过程、密码、session、proof |
DELETE /me R | {current_password,confirm:"DELETE"} | 202 {deletion_requested:true};事务设 deleting、增加 auth_version、写删除记录和任务、清 Cookie |
删除后的匿名重试仅返回通用 202 收件确认,不透露账号或删除状态;不得绕过首次删除所需重新验证。注册/改密/删除等失败都保留本地游戏数据,由玩家另选是否清除此设备。
merge preview 明确输出 {xp_before_units,xp_after_units,legacy_baseline_policy:"max",daily_duplicates,run_duplicates,save_branches,warnings},以及两端 data_revision。票据签发的源会话允许因“登录目标账号”被撤销后继续使用票据;改密、账号删除/合并/停用则使票据失效。源是游客且目标是注册用户由事务核验。目标头像/昵称默认保持,设置默认目标优先,源存档全部按分支保留,个人录像保持本机归属,不上传至合并接口。
5. 同步入口与数据操作#
5.1 客户端发送规则#
POST /sync/push G。body {device_id,epoch,operations:[Operation]}。首期 JSON 请求不接受 HTTP Content-Encoding 压缩,避免与游戏 blob 压缩混淆;1 MiB 为原始 UTF-8 字节上限。最大 100 操作。device 必须属于会话玩家;正常同步只允许 session.device_id。客户端每档案一个发送者、一个在途 batch,同一实体依赖按提交顺序排列。
Operation 闭合字段:op_id:UUID, type:枚举, schema_version:1, entity_id:string(1..200), base_version:非负整数, depends_on?:UUID[], client_time?:UTC毫秒, payload:该type对象。依赖最大 10,禁止循环/自依赖;payload 摘要按 RFC 8785 canonical JSON 对整个 Operation 计算 SHA-256,拒绝不兼容数值,不包含请求包络和头。
base_version=0 表示创建且实体必须不存在(并且不在 retired_entities);update/delete 必须匹配当前版本。不可变对局按业务键核对原内容,不使用时间戳覆盖。
| type / entity_id | payload 字段 | 校验与服务端产物 |
|---|---|---|
run.create / run_id | {origin_id,mode,content_id,content_revision,rules_revision,config} | 注册元信息;配置对照模式规范及已发布内容;origin_id 与 run_id 对新局相同,导入保留旧来源 |
run.finish / run_id | {status,elapsed_ms,opened_safe,safe_total,score_units,assist_class,daily?,final_snapshot} | status=won/lost/abandoned;run 必须存在;验证器检查结果和计分并生成 run_results、奖励、进度、事实、summary,客户端不能指定 provenance/earned_units;只生成client_reported私人结果,不能覆盖server_validated桥接终局 |
save.create / slot_id | {run_id,branch_id,format_version,snapshot} | 创建分支,device 从 session 获取;同 run/branch 只一份 slot |
save.update / slot_id | {format_version,snapshot} | 原子版本检查;不允许修改 run_id/branch_id |
save.delete / slot_id | {} | 清 snapshot,版本 +1,写墓碑与 retired_entities |
setting.set / field_key | {value} | 按字段 CAS;同组有联动约束的 combo 整组提交 |
lesson.complete / lesson_id | {catalog_revision} | 仅已发布手册课 ID;每课完成事实唯一,client_reported;无经验,可能解锁对应勋章 |
legacy.import / origin_id | {blob_id} | blob.purpose=legacy_import;应用版本化迁移器生成历史基线、通关/每日事实及存档;fingerprint重复不再入账 |
新协议的私人daily run.create/run.finish/save.*只允许服务器已结束日期的历史练习包,校验content_id/map_hash及发布映射;当前日返回ONLINE_DAILY_REQUIRED,未来日期返回DAILY_NOT_YET_AVAILABLE,不能上传伪造的今日本地终局领取正式资格。旧版日期成果仅走legacy迁移并标client_reported。
run.finish 的 daily 对象仅 daily 模式必填 {challenge_date,tier},与 daily_challenges、run 配置一致;其他模式禁止该字段。整数用时 ≥0,opened_safe∈[0,safe_total],safe_total与地图相符,score_units≥0且不超过该 revision 的算法上界。assist_class 来自已注册枚举 manual / ai / hint / undo,私人结果仅按上传声明标记;字段/快照不一致可以拒绝,不能从终局证明未使用辅助。私人上传只能标记 client_reported。
config/snapshot/final_snapshot 使用模式注册表中固定的 JSON schema 和恢复器;schema 由 (mode,content_revision,format_version或rules_revision) 唯一定位,不接受任意对象直写数据库。classic/custom/daily 的棋盘对应 MineGame.serialize/restore;campaign 包含路线 revision、阶段及 Campaign.serialize/restore;field_lab 对应固定关卡引擎快照;sudoku 包含 current/history/future/elapsed;single_mine 包含 state/elapsed/started/selected。实施时必须从各模式现行序列化结构冻结 schema fixture 后才启用该模式的上传,不能把“前端能 parse JSON”作为验证标准。
setting.set 的值:theme=system|light|dark;sound=boolean;racing_mode=boolean;appearance 为发布清单中的悬停/格子/旗帜颜色 ID 对象;combo={enabled:boolean,window_ms:300..3000,show_countdown:boolean}。联动值沿用游戏设置定义并在协议发布时锁定。设备缩放与窗口信息不接受同步。进度、经验、勋章没有任意 PATCH 接口。云端snapshot/final_snapshot/legacy导入必须按白名单剔除replays、replayDraft、录制事件序列;普通棋盘续局仍可保存,不能把个人录像伪装成证据上传。个人录像迁移只在本机完成。
私人快照只做结构与规则上界验证,客户端结果仍为client_reported。服务器在线过程独立裁决后由内部桥接写server_validated;完整正式每日记录不通过本接口上传。验证在事务外做,事务内重新检查规则和原实体版本。
5.2 回执#
HTTP 200 表示批次语法和认证成功,每个操作独立结果;语法不合法或身份错误在整批执行前拒绝。单条结果:
{
"data": {
"results": [{
"op_id": "20000000-0000-4000-8000-000000000001",
"status": "accepted",
"receipt": {
"entity_id": "30000000-0000-4000-8000-000000000001",
"version": 1,
"effects": [{"type":"reward","delta_units":13800}],
"changes": [{"entity_type":"summary","entity_id":"self","version":9,"value":{"xp_units":25000,"played":12,"wins":8,"best":{}}}]
}
}]
},
"meta": {"request_id":"req-example","server_time":1789171200000,"protocol_version":1}
}status=accepted|duplicate|conflict|rejected|dependency_pending|deferred。duplicate 仅外层状态不同,receipt 与原结果字节语义一致。conflict/rejected 含 error:{code,details};conflict 包含服务端实体版本和快照下载/读取信息,本地保留副本。终局同 run 相同 payload 但不同 op 返回业务重复、无再次奖励;同 run 不同终局返回 RUN_ALREADY_SETTLED。
幂等最终回执在同一 SQLite 事务持久化;conflict/rejected 也可持久化为终态,修正请求需新 op_id。dependency_pending、deferred、503 不写最终回执;依赖窗口已过期则 RESET_REQUIRED。已拒绝依赖返回 DEPENDENCY_FAILED,客户端隔离其后续链。历史练习所需的日期尚未结束时返回 deferred 和 retry_at=ends_at,不占首通、不写终局;已保存的旧版本地成果保持在本机,不能将等待同步解释为允许生成今日题。
响应不是 pull 游标,客户端不能用 push 最新 seq 跳过其他设备未拉取的变更。回执用于确认本地 outbox 和基线,后续 pull 重复内容按实体版本去重;更高版本的本地待发编辑在基线上重放。
5.3 同步拉取与快照#
| 路由 / 权限 | 参数 | 输出 |
|---|---|---|
POST /sync/bootstrap G | {} + Idempotency-Key | 202 {job_id};创建一致性快照任务,重复 key 返回同一任务 |
GET /sync/bootstrap/:job_id G | 无 | {state,artifact_id?,manifest?};manifest含页数、每页hash、epoch、H的签名游标、expires_at |
GET /sync/bootstrap/:job_id/pages/:index G | 无 | 不可变 JSON 页 {entities:[{entity_type,entity_id,version,value}]} |
GET /sync/pull G | cursor,limit? | {changes:[{entity_type,entity_id,version,value或deleted:true}],next_cursor,has_more},最多200条并受1MiB页限制 |
POST /sync/ack G | {device_id,cursor} | 204,验证游标归属/epoch/签名后单调更新 ack_seq |
首次必须 bootstrap,禁止拿 cursor=0 绕过全量历史基线。只导出客户端需要的实体:profile、settings、summary、daily、level_progress、medals、save、必要run元信息和已确认终局;不导出密码、会话、内部审计、账本内部调整细节。
游标内部包含 {user_id,epoch,after_seq,high_water_seq?,issued_at} 并签名,编码为不透明字符串。一次 pull 首次固定 H,分页结束关闭本轮 H,下次再读取新高水位。服务器只取该玩家 (after_seq,H] 的日志;响应 value 必须是日志当时内容。200条不足1MiB按字节优先截断,单实体过大则响应授权 artifact 引用并占一条变化,客户端取回前不能 ack。
bootstrap 任务在短只读快照中导出状态与 H 至有界暂存,再释放读事务,将不可变页面入库后标记 done;不得分页期间继续读取活的实体行。任务失败不发布部分 manifest,客户端验证全部页后原子切换并保留 outbox;artifact TTL 30分钟,过期重新申请,重建期间游戏可继续。
每玩家 sync_state 记录 epoch 与日志保留水位。日志过期或备份恢复 epoch 变化返回 RESET_REQUIRED。ack 仅证明客户端声明已应用,不作为永远删除全部历史的唯一依据。私人档案无需逐步实时传输,通知只提示“有新数据”;正式每日使用独立WSS实时输入,不能用本节pull驱动比赛裁决。
6. 旧档案导入文件与内容目录#
POST /blobs G只接受X-Blob-Purpose: legacy_import,Content-Type为application/octet-stream,支持X-Blob-Codec:gzip|identity和X-Content-SHA256。仅用于去除个人录像/录制草稿后的旧进度迁移包,采用版本化白名单;replay/evidence目的和任何录像字段拒绝,不落盘。临时缓冲解码验证后才入SQLite。
原始文件大小8MiB、解压32MiB、每人暂存128MiB为沿用的[待测试]初值;同用户/hash幂等返回blob_id,不同账号不共享查询结果。导入任务读取后保留来源指纹和迁移结果,文件24小时后回收;未完成受控任务暂缓回收。它不是云盘,也不保存个人录像。
GET /blobs/:id G只可读取本人尚存的legacy_import临时文件;返回attachment/no-store/nosniff,无公开直链,过期410。公开Top10回放仅走watch授权路由,官方候选证据仅在授权后上传,历史回放取消通关门槛。个人录像手动导出使用本地文件操作,不调用/me/export或/blobs。
GET /catalog P 返回 {server_time,manifest_revision,min_protocol_version,rules:[{revision,offline_import_allowed}],content_manifest};服务端时间不缓存到静态目录正文,公开不可变目录单独带 ETag。未知 revision 在本地保留,不根据字符串相似度尝试发奖。每日date/tier映射由服务器生成验证后发布,目录只含公开元数据,不能含当天隐藏盘/seed。历史补打允许并去重领奖,未来题不得提前下载;现有本地旧题仅按legacy私人成果迁移,不能生成当前正式题。今日与历史题包接口见多人API。
7. 完整时序#
sequenceDiagram participant P as 玩家/本地档案 participant C as 同步客户端 participant S as API + SQLite P->>P: 离线操作、结算与outbox同事务保存 P->>C: 网络恢复 C->>S: GET auth/session S-->>C: 身份、CSRF、服务器时间 C->>S: bootstrap或pull已有云变化 S-->>C: 一致性基线/增量 C->>C: 应用基线并重放待发操作 C->>S: push稳定op_id与事实 S->>S: 校验、去重、奖励、变更、回执同事务 S-->>C: accepted或原回执或冲突 C->>C: 确认outbox、保留冲突副本 C->>S: pull并ack(不跳过其他设备数据) C-->>P: 已同步/选择继续哪份棋局
查看流程图源文
sequenceDiagram
participant P as 玩家/本地档案
participant C as 同步客户端
participant S as API + SQLite
P->>P: 离线操作、结算与outbox同事务保存
P->>C: 网络恢复
C->>S: GET auth/session
S-->>C: 身份、CSRF、服务器时间
C->>S: bootstrap或pull已有云变化
S-->>C: 一致性基线/增量
C->>C: 应用基线并重放待发操作
C->>S: push稳定op_id与事实
S->>S: 校验、去重、奖励、变更、回执同事务
S-->>C: accepted或原回执或冲突
C->>C: 确认outbox、保留冲突副本
C->>S: pull并ack(不跳过其他设备数据)
C-->>P: 已同步/选择继续哪份棋局后续 SSO 路由仅预留,不在本版本注册可用 handler:POST /auth/sso/:provider/start、GET /auth/sso/:provider/callback、POST /me/identities/:provider/link、DELETE /me/identities/:id。state、nonce、PKCE、issuer/subject唯一与最近重新认证另行冻结契约,禁止通过邮箱相同自动绑定。
8. 单实例请求保护#
首期 API 使用单进程有界 token bucket,反向代理限制连接、请求体和来源请求频率。限流计数只存在进程内;账号、会话、奖励、幂等回执和可靠任务仍保存在 SQLite。账号与私人进度读取直接查SQLite,需要一致性时用短读事务;这些私有数据不进Redis。仅每日榜单快照使用Redis缓存,官方录像正文使用Redis TTL存储,见Redis设计。
沿用前版请求保护初值,全部 [待测试]。B 为桶容量,r 为恢复速率;表中每个 action、每个维度分别建桶,请求必须满足所有适用限制。
| 请求 / 维度 | B / r | 调参区间 | 依据 |
|---|---|---|---|
| login / IP | 30次 / 30次每分钟 | B和每分钟r均15..60 | 容忍共享网络,同时控制密码哈希负载 |
| login / 规范账号 | 10次 / 10次每15分钟 | B和每15分钟r均5..20 | 防换IP猜同账号,短时限制不封禁账号 |
| guest、register / IP | 各10次 / 10次每小时 | B和每小时r均5..30 | 约束重复创建账号与游客档案 |
| recover、change、rotate / IP | 各10次 / 10次每小时 | B和每小时r均5..20 | 身份变更正常低频 |
| recover / 规范账号 | 5次 / 5次每小时 | B和每小时r均3..10 | 约束恢复码尝试 |
| change、rotate、merge、delete / 玩家 | 各5次 / 5次每10分钟 | B和每10分钟r均3..10 | 约束重复昂贵或敏感操作 |
| sync / 玩家 | 30请求 / 60请求每分钟 | B10..60,r30..120每分钟 | 支持周期同步和结算突发 |
| sync操作数 / 玩家 | 300条 / 600条每分钟 | B100..1000,r200..2000每分钟 | 每批按实际操作数扣除,防批量绕过 |
| blob字节 / 玩家 | 16MiB / 16MiB每分钟 | B8..32MiB,r8..64MiB每分钟 | 容纳单条8MiB文件及网络重试 |
| bootstrap、export / 玩家 | 各3次 / 3次每10分钟 | B和每10分钟r均2..5 | 控制全量快照负载 |
用服务端单调时钟计算 tokens=min(B,tokens+elapsed_seconds*r);令cost为请求数、操作数或字节数。单次不含await的临界区完成所有适用桶的检查和扣减;不足时返回429,Retry-After=ceil(max((cost-tokens)/r))。不接受客户端时间或客户端自报身份作为限流权威。认证成功、失败和幂等重试都占请求额度,已验证receipt命中不重复执行哈希。
IP/账号维度使用服务端密钥HMAC后的标识,只信任受控代理的来源IP。上传先按合法Content-Length预扣并强制流式字节上限,不接受缺失/伪造长度绕过;持久文件总配额仍在SQLite事务内检查。
进程内最多10,000个活跃桶初值 [待测试],依据是初期内存有界。已恢复满额且空闲的桶可清理;未恢复满额的桶不能被淘汰后重新按满桶放行。达到上限时,新维度使用各action同参数的共享保守桶,或对认证请求返回503。全局哈希工作池初值2并发、最多排队20个、等待超过2秒返回503 [待测试],按64MiB Argon2id初值计算哈希工作内存约128MiB加进程开销,需部署压测。
进程重启会重置限流计数,允许一次新的B突发;反向代理保护仍运行,数据库中的会话吊销和奖励去重不会重置。首期不部署多个API进程共享这套内存额度;增加实例或worker进程前必须重新定义总体限额。
验收:并发通过量不超过B+r×时长;内存满不无限放行;进程重启后不重复入账;账号登录被限流时本地仍可玩;SQLite不可用返回503且不确认同步成功。这些在线行为待实现后验证。
9. 版本记录#
| 版本 | 日期 | 变化 |
|---|---|---|
| 0.8.1 | 2026-09-13 | 收紧录像大小/事件上限;双hash上传前去重;补流式拒收、配额、慢连接及验证资源保护 |
| 0.8.0 | 2026-09-13 | Redis限每日榜单缓存与官方录像TTL存储;录像正文移出SQLite,不设录像到期清理job;补跨库故障与持久化边界 |
| 0.7.0 | 2026-09-13 | 移除直播入口/协议/三张专用表;观看仅限每日公榜录像,保留四榜与私人同步;整理审阅范围 |
| 0.6.0 | 2026-09-13 | 初中高与综合四榜独立;单项1段/综合3段,录像共享、统一冻结、跨端分段提交;48项DDL检查通过 |
| 0.5.0 | 2026-09-13 | 私人每日仅历史练习;官方候选压缩上传独立接口;补多端对账与脱敏冻结引用边界 |
| 0.4.0 | 2026-09-13 | 个人录像仅本机,移除个人云录像操作 |