Files
airz-uniapp/docs/积分规则与开发契约.md
T
hujun 2a9ff403c4 feat: 接入内容服务端真实接口并优化页面交互
- 迁移内容数据到服务端接口,移除本地模拟数据
- 首页新增推荐课程加载与未读消息检查
- 资讯、课程、搜索页面支持分页加载与错误重试
- 登录页新增恢复课程、消息、帮助页的路由逻辑
- 评估分析页新增处理中状态动效与阶段提示
- API 环境切换到正式域名 airz.hqling.cloud
2026-10-01 20:26:41 +08:00

200 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 积分规则与开发契约
记录日期:2026-10-01。版本:points-backend-20261001-v2。状态:**后端及真实HTTP隔离开发检查完成,业务规则未定稿,默认不入账,原表未迁移、未部署**。归属既有 POINT-001—004,不新增任务编号。POINT-002保持开发中;DEC-POINT/POINT-001仍待确认,POINT-003未实施,POINT-004页面尚未接真实积分。
本文件作为积分实施入口;数据库具体增量见[积分数据库方案](D:/AAdesk/202609/airz/database/points-design.md)。接口源码与真实HTTP隔离检查已完成,未部署;页面仍是本机体验签到。未获规则确认时不填真实奖励,配置默认false/null。
## 0. 最新协议与隔离开发检查
87个真实HTTP请求/1079项检查通过,经过实际ThinkPHP路由、Redis Token鉴权和积分控制器。覆盖匿名/无效/停用/删除/撤销、Cookie/旧Token头拒绝、允许Origin的匿名OPTIONS、JSON/原始query/错误方法、草案/发布/暂停/坏规则、跨日/版本变化、实际丢弃已提交成功响应后的原键确认、本人快照/隐私/旧NULL与0流水、余额边界、坏回执和Redis不可用。
新脚本tests/points_http.php仅取明确私密环境的本机3309连接,创建独立合成数据库、临时ROOT/RUNTIME、随机ENV_PREFIX和Redis DB15随机Token作用域;不读取正式.env,不启动worker或调用模型。成功后自建数据库、Token、临时服务/文件已清理,共享fa_user/fa_user_score_log结构/数据及原Redis队列摘要保持。所有权标记限制目录清理;清理失败返回非0并保留受限目录及安全定位,不能声称资源已移除。
主服务238项、HTTP输入190项纯检查通过。原表23项只读预检通过,仍4增量、14类质量计数各项0,applied=false。迁移新增非NULL历史时间质量计数,正UINT流水ID并入invalidLedgerIntegers;合法NULL历史时间保留,非NULL时间范围0—253402271999,确保上海ISO8601四位年份。首次快照坏主键固定409/POINT_LEDGER_INVALID。当前业务配置保持false/null,页面、原表迁移及部署未完成。
迁移纯110项、独立合成库apply/备份/Windows ACL/恢复89项通过。共享隔离库只复制结构,第一新库合成2会员4流水;apply4增量、重复为0,完整双表schema+NDJSON恢复第二新库并逐列摘要一致。目录3文件ACL及读写probe通过,修正Windows原生绝对路径及父级路径校验,纯路径边界覆盖;两新库、私密INI/备份/probe已清理,源表摘要保持。安全verification保留于C:/Users/JUN/AppData/Local/Temp/airz-points-migration-20261001/apply-20261001-115749-d50a5cd87fd5a84a/verification.json。仅为开发演练,未执行共享原表DDL或生产操作。
### 0.1 历史:0.48后端基础检查
已新增 PointsRules、UserPoints、v1 Points 控制器、五条路由、默认未发布配置、显式迁移脚本及测试。通用后台score/level、通用模型更新与旧User::score均已拒绝写入;新服务直接在账户锁内限定更新score并保存完整流水,不重算level。新会员0分、数据库默认0级及Auth注册1级初始化保持。schema.sql和ScoreLog中文注释已同步目标结构,不能据此声称目标库已经增加字段。
纯检查237项服务、98项写保护、84项迁移通过。隔离MySQL3309随机前缀合成表45项检查通过,包含同键/异键双PHP并发、锁等待跨日和真实流水insert故障回滚;测试表及临时文件已精确清理。原fa_user/fa_user_score_log结构和数据摘要保持。另22项只读迁移入口检查通过,重复预检仍为4增量、13类质量计数均0,applied=false;本轮修复PHP7.4 PDO不追踪原始START事务导致预检commit失败,改PDO事务接口与RR只读快照。
没有迁移原表、运行apply/备份/Windows ACL、调用模型/worker/Redis/HTTP、编译页面或部署。提交结果未知和连接故障由纯替身覆盖,不是实际断网或生产到账证据;页面状态、真实HTTP、迁移应用与恢复仍须后续开发检查。
## 1. 首阶段建议与待确认项
建议先实施 POINT-002 的账户余额、流水、每日签到,再接 POINT-004 对应页面部分。POINT-003 邀请关系和可信奖励独立实施;不因完成签到而标记整个积分模块完成。
| 项目 | 首阶段建议 | 状态与理由 |
| --- | --- | --- |
| 每日签到 | 已登录且状态正常的会员,每个业务日一次,每次 10 分 | **待确认数额**;沿用既有页面建议,不表示已批准真实奖励 |
| 业务日 | 服务端 Asia/Shanghai,00:00 至次日 00:00,不提供补签 | 待确认;设备时间、时区及登录连续天数不作为依据 |
| 连续签到加成 | 不启用,始终固定当期奖励 | 待确认;不需要新增连续签到表 |
| 完善资料奖励 | 首阶段关闭,奖励数额为 null | 必须后续确定完整标准、可信事件和一次性范围;进入档案或编辑资料不发奖 |
| 邀请奖励 | 首阶段关闭,奖励数额为 null | 有效邀请条件、双方奖励、次数和归因期限待定;复制、分享、绑定码不直接发奖 |
| 积分用途 | 首阶段只提供余额与流水,不启用提现、现金兑换、AI 次数抵扣或贷款优惠 | **待确认权益范围**;不能把积分说成授信、信用分、放款条件 |
| 有效期 | 首阶段建议不自动过期 | **待确认**;若需要到期扣减,须先设计到期批次/消费抵扣,不靠定时清零余额实现 |
| 会员等级 | 新签到服务首阶段建议仅变更 score,不新增等级或权益联动 | **待确认**;旧 User::score 会按余额改 level,需要明确兼容处理后实施 |
| 历史体验积分 | 不导入、不补发真实积分 | 本机记录可篡改,匿名也可签到;正式余额只取服务端既有 score |
| 历史服务端数据 | 原 score、流水、日期、备注、业务键保留 | 不按流水合计重建余额、不自动清零、不擅自修复差异 |
首阶段的业务确认应至少覆盖“每天 10 分、上海每日一次、不补签”“资料/邀请奖励关闭”“权益、有效期及等级处理”。在明确确认前,设计状态保持 draft;未来配置的安全默认值必须为未发布、停止新入账,奖励数额不得用页面示例兜底。
## 2. 已核实的源码现状
| 位置 | 当前能力或风险 | 实施处理 |
| --- | --- | --- |
| [积分页面](../packages/account/points/points.uvue) | 余额为本机签到日期数乘 10;设备日期;固定体验码 AI2680;没有积分 API | 接真实 API 后移除该余额算法及固定码,清理体验缓存但不换算奖励 |
| [账户服务](../packages/account/services/account.uts) | 已有登录、账户变化防护;资料 DTO 无 score/share_code | 新增同分包积分服务,复用会话而不扩充无关资料字段 |
| [请求基础](../services/api.uts) | 有 Token 变化及 401 防护;当前错误仅保留 message | 实施时保留现有调用兼容,增加可识别的 HTTP 状态/errorCode,区别确定失败与结果未知 |
| [数据库结构](D:/AAdesk/202609/airz/database/schema.sql) | fa_user.score 为有符号 INT;流水已具业务键、事件唯一及用户时间索引 | 复用余额和流水;目标库实际结构仍须迁移前只读核实 |
| [通用积分方法](D:/AAdesk/202609/airz/application/common/model/User.php) | 旧score()已明确拒绝,通用模型不能更新score/level | 使用UserPoints领域服务,不再吞异常或随积分重算等级 |
| [后台会员模型](D:/AAdesk/202609/airz/application/admin/model/User.php) | 模型事件、后台增改/批量及编辑表单已保护score/level | 积分/等级只读;任意SQL仍不经过模型守卫,新增写方须遵守共同账户锁 |
最初设计依据源码和schema.sql;本轮仅对显式隔离库读取实际结构/数据并在随机合成表检查事务。正式库未连接,隔离原积分表仍缺两个新列和两个索引,不能把合成表或结构文件当作正式迁移证据。
## 3. 规则发布与事件身份
已增加application/extra/points.php,独立于模型调用额度。默认 `enabled=false`,rule_version/check_in_reward/expiry_policy/level_policy/benefits_policy均null;API映射ruleVersion/checkInReward。发布必须提供完整版本、1—1000的规范整数奖励及none/unchanged/none三项策略;这一范围是配置校验上限,不是批准的奖励额。enabled=false且保留完整发布内容表示paused,不删历史回执。损坏/不完整配置固定POINT_RULE_UNAVAILABLE。
规则版本格式为 2—64 位小写 ASCII,`^[a-z][a-z0-9-]{1,63}$`;拟首版名 `points-checkin-20261001-v1`。同一版本永久对应同一奖励额、日期和参与语义;变更必须新版本,不改历史流水。暂停活动不删除成功回执;启用配置编辑后台不属于首阶段,先使用受控部署配置。
每日签到的账本事件固定:
```text
business_type = checkin
business_id = 服务端上海业务日 YYYY-MM-DD
unique = user_id + business_type + business_id
```
事件键不包含规则版本,因此同一天发布新版本也不能重复奖励。新请求只能为当前业务日入账;设备日期和客户端字段不能指定补签。旧已成功请求允许在下一日、规则暂停或新版本发布后返回原回执,但仍要求当前账户有效且拥有该流水。
## 4. 接口源码契约
统一前缀 `/api/v1`,全部要求显式合法Bearer及当前正常会员,不接受Cookie/旧Token头回退。沿用v1 Base的code/msg/data与data.errorCode、Cache-Control: no-store,v1框架错误亦补该缓存头。由认证身份取得userId;未知参数、客户端余额/奖励/业务完成状态/其他用户ID均拒绝。五条精确白名单路径已实现并通过HTTP开发检查,原表迁移/规则发布/页面尚未完成,不视为线上能力。
签到要求application/json,可带UTF-8 charset;正文由原生JSON解码及领域校验,必须三个唯一字符串字段,重复键和Unicode等价重复键拒绝。原始QUERY_STRING校验唯一解码键、白名单及PHP解析一致;NUL、坏编码、重复和空片段拒绝。原始/有效HTTP方法须与接口方法一致,覆盖头存在即拒绝;错误方法405,未知路由404。Common既有入口在鉴权前处理允许Origin的匿名OPTIONS,不重复实现CORS。
| 方法与路径 | 严格输入 | data 主要字段 |
| --- | --- | --- |
| GET /points | 无业务参数 | balance:int、serverDate、timezone、signedToday、checkInEnabled、ruleVersion:string/null、checkInReward:int/null、balanceState:normal/anomaly |
| GET /points/rules | 无业务参数 | status:draft/published/paused、ruleVersion:string/null、checkInReward:int/null、timezone、publishedRules;未发布规则不显示为已生效 |
| GET /points/ledger | limit 默认20/最大50;后续页 beforeId + snapshotId | items、snapshotId、nextBeforeId;本人流水,ID 字符串 |
| POST /points/check-in | requestId、expectedRuleVersion、expectedDate,必须恰好三个字段 | ledgerId、delta:int、balanceAfter:int、signedDate、ruleVersion、replayed:bool |
| GET /points/tasks | 无业务参数 | 首阶段只返回已发布签到任务;未实施资料/邀请奖励明确 unavailable,reward=null |
流水源码采用与现有消息/内容一致的有界游标,替代旧page/pageSize目标草案;没有已发布积分API,不涉及接口兼容迁移。tasks实际返回serverDate/timezone/items,签到任务按已发布规则和今日状态显示,资料/邀请始终unavailable且reward=null。邀请/invitations和/invitations/bind保留后续阶段目标,不返回固定码或假的奖励进度。
### 4.1 签到请求及回执
`requestId` 为 `pts_` 加 32 位小写十六进制,客户端用安全随机数生成,同一用户命名空间内唯一。`expectedRuleVersion` 为页面实际读取的已发布版本;`expectedDate` 为同一次读取返回的 serverDate,是操作预期条件,**不是指定奖励日期**。日期须为真实有效的 YYYY-MM-DD,拒绝非法日历日期、空值、数组和宽松类型转换。
示例仅描述获批发布后的协议形状,当前无这样的运行规则:
```json
{
"requestId": "pts_0123456789abcdef0123456789abcdef",
"expectedRuleVersion": "points-checkin-20261001-v1",
"expectedDate": "2026-10-01"
}
```
入账回执与原流水绑定;重放中 `replayed=true`,其余业务字段保持原值。`balanceAfter` 是这笔入账后的历史余额,不是当前余额;页面必须随后重新 GET /points,不能用旧回执覆盖新余额。不同请求键同日重复签到返回明确的 `POINT_ALREADY_SIGNED`,不写第二条零分流水,也不把新的请求键附到原流水。
### 4.2 余额与流水读取
余额、今日签到状态与规则须来自一次一致的读取:先按当前会员事务读取,固定一个服务端业务日,再完成对应状态查询,避免日界/入账途中混合视图。日期和规则必须一起提供,客户端不能用本机时钟补齐。服务不可读时固定失败,不把错误或未知余额转成 0。
流水DTO为ledgerId/delta/balanceBefore/balanceAfter/businessType/businessDate/ruleVersion/createdAt。时间为ISO8601带时区,历史未知时间保留null;日期为上海YYYY-MM-DD。ID为字符串,支持现有无符号INT至4294967295,服务/迁移要求64位PHP。历史未识别事件统一legacy,业务日/版本可null;不返回原memo、原业务标识或请求键。历史真实0分记录如实展示,不因新服务禁止零分占位而删除。
首次列表取得本会员最大 ID 作为 snapshotId,空为字符串 "0";后续固定上界并按 `id<beforeId` 倒序读取,nextBeforeId=null 表示末页。上界/游标须属于当前用户;不存在或他人标识统一 `POINT_SNAPSHOT_INVALID`,不泄露归属。新流水不插入已翻页窗口,刷新重新取得上界。
负余额保留真实读数并标 balanceState=anomaly,停止新签到等待核对,不静默修正。本轮不根据历史流水合计断言账户余额错误;可能存在早期余额或未完整记录的合法调整。
### 4.3 固定错误
| HTTP / errorCode | 含义与页面处理 |
| --- | --- |
| 401 / AUTH_REQUIRED | 登录或账户状态失效;清当前账户状态,固定登录恢复 |
| 503 / AUTH_STORAGE_UNAVAILABLE | Token存储不可用;不当作已退出或业务未入账,保留待确认请求 |
| 405 / METHOD_NOT_ALLOWED | 原始/有效方法不符或使用方法覆盖;不进入账本操作 |
| 422 / POINT_INPUT_INVALID | 请求字段、类型、ID、版本或日期非法;不入账 |
| 409 / POINT_REQUEST_CONFLICT | 成功请求键已用于另一日期/版本;保留原账本,不改意图重试 |
| 409 / POINT_DATE_CHANGED | 未成功的预期日期不是锁后服务端当前日;刷新,用户重新点击才生成新请求 |
| 409 / POINT_RULE_CHANGED | 预期版本与当前有效版本不同;刷新后重新确认 |
| 409 / POINT_ALREADY_SIGNED | 当前日已有签到;刷新状态,不再次显示增加积分 |
| 409 / POINT_SNAPSHOT_INVALID | 流水游标无效;清旧列表并重新查询 |
| 503 / POINT_RULE_UNAVAILABLE | 规则未发布、暂停、损坏或不可读;关闭新签到,不以 10 分兜底 |
| 409 / POINT_BALANCE_INVALID | 负余额、非规范余额或本次加法越过 INT 上限;不更新 |
| 409 / POINT_WRITE_CONFLICT | 确定的1062/1205/1213竞争且本请求事务已回滚;不自动换键,可用原请求重试 |
| 503 / POINT_LEDGER_UNAVAILABLE | 账本查询/写入暂不可用,或提交/回滚结果无法确认;客户端保留原请求确认结果 |
| 409 / POINT_LEDGER_INVALID | 成功回执元数据损坏或业务唯一键与预期记录不符;不覆盖、不伪造回执 |
网络失败和无法确认的 5xx 不表示未入账。异常原文、SQL、内部连接、其他会员信息和凭据均不下发。
## 5. 事务与重放算法
1. 验证恰好三个字段,规范化按固定字段顺序处理,拒绝 GET query 或其他正文参数。以当前认证用户确定账户。
2. 开启事务,锁定 fa_user 当前行,重新核验 status=normal。账户不存在/禁用不能借历史请求重放。
3. 在用户锁之后以当前读查 `(user_id,request_id)` 的已成功流水。命中时核验 `business_type=checkin`、规范日期、规则版本、delta/before/after 算术及请求预期;同意图返回原回执,异意图固定冲突。历史 NULL 元字段不能充当该回执。
4. 未命中时,在取得用户锁后采样一个上海业务日与时间,校验 expectedDate。锁等待跨午夜必须使用锁后日期;不匹配先返回 POINT_DATE_CHANGED。
5. 以当前读检查固定 `checkin + 当日` 事件;已有则返回 POINT_ALREADY_SIGNED,优先于活动暂停或新规则变化。未签到才读取有效发布规则并校验活动启用和 expectedRuleVersion。业务键不因新请求或新规则版本变化。
6. 校验现有余额及奖励的整数边界,计算 after=before+reward,要求 0≤after≤2147483647。只进行正数签到奖励,负数调整与消费另行设计。
7. 同事务更新用户 score、写一条完整积分流水(事件键、请求键、规则版本、before/after/delta)。任一步失败全部回滚,不能吞异常或写零分占位。
8. 提交成功后返回流水冻结回执。确定1062/1205/1213且确认本请求事务已回滚时返回 POINT_WRITE_CONFLICT,不自动换请求键或无界重试;提交/回滚无法确认时返回 POINT_LEDGER_UNAVAILABLE,用原请求键确认。处理框架回滚异常时保留实际业务失败,不暴露原异常。
所有影响入账的数据库读取使用当前读,不能复用在用户锁前形成的 MySQL RR 快照或外层事务旧视图。规则若采用部署配置,必须每次操作读取该进程当前已发布配置;版本与语义不可变。跨实例滚动部署出现版本不一致时,固定规则冲突优先于偷偷改变奖励。
```mermaid
flowchart TD
A[验证登录和三个预期参数] --> B[锁账户并查成功请求]
B -->|同意图命中| C[返回原流水回执]
B -->|未命中| D[锁后日期校验]
D --> E[检查当日事件 再校验规则与积分边界]
E --> F[余额和一条流水同事务提交]
D -->|日期或版本变化| G[固定冲突并刷新]
E -->|已签到| G
```
## 6. 首阶段数据库与写路径
schema.sql及显式迁移脚本已定义可空request_id/rule_version、唯一(user_id,request_id)和ID游标索引(user_id,id);原表未应用,旧流水保持。已有业务唯一和用户时间索引不重复添加;用户时间索引不能直接保证按ID排序。成功流水保存日唯一和请求回执,不建独立签到/请求/连续签到表。具体字段、中文注释与预检见[数据库增量方案](D:/AAdesk/202609/airz/database/points-design.md)。
本轮已盘点并封闭通用后台绝对值编辑、批量、User::score和模型score/level更新;仅新增服务加锁不能防其他旧写方覆盖余额。受控管理员积分调整独立排期,当前只读;任意SQL不经过模型守卫,后续插件/导入必须重新审查。会员删除的既有历史策略本轮保持,可能有孤儿流水;已删除/停用账户不能通过新服务读取或重放。
新服务拒绝外层事务,确保成功意味着本次真正提交;不回滚调用方已有事务。数据库break_reconnect必须严格false,防断线自动重连使单条SQL意外自动提交。事务竞争只在确认本人事务回滚后映射409,未知提交返回503保留原键,不进行自动SQL重试。
后续邀请关系需独立表,受邀人唯一绑定、禁止自邀、邀请码唯一且历史冲突先人工处理。资料奖励以永久任务事件去重,不能按每次资料修订发奖。只有服务端可信业务事件可触发奖励;不能用客户端声称“资料已完成/好友已注册”发奖。
## 7. 页面实施边界
- 登录后恢复积分页需为现有登录白名单增加固定 `resumePoints` 目标,不接受任意回跳 URL。
- 初次/刷新分别处理加载、失败、空流水、未发布规则、已签到;失败清掉不可信旧状态,不显示假的 0 分或成功。
- 发送中锁定签到按钮;请求结果未知时保留原 requestId/date/version 和账户身份,只允许原请求确认。不能因刷新、关闭提示或跨日自动生成另一签到请求。
- 原回执仅确认原业务日;回执后重新读取当前余额和 serverDate。要签到新一天,必须由用户在刷新后明确点击。
- 离页/换账号拒绝迟到响应,未确认请求随账户作用域隔离;未知请求保留范围先按当前页面会话,不跨退出/换账号迁移。
- 从 API 的 ruleVersion/reward 显示规则与奖励,不再写死 10、AI2680 或本机积分。未接邀请前不显示“真实个人邀请码”或未发生的邀请奖励。
- 清理体验签到缓存不产生服务器写入;原匿名/本机记录不影响服务器 signedToday。
## 8. 后续开发检查清单与顺序
以下为完整开发检查范围;已覆盖部分见第0节。HTTP后端隔离检查已完成,页面和原表迁移/正式发布仍未完成:
| 检查 | 关键反例 |
| --- | --- |
| 并发入账 | 同日同键/异键双进程;只有一笔奖励和流水,余额正确 |
| 历史重放 | 第一天成功、第二天原键仍原回执;新日期复用成功键拒绝 |
| 日期边界 | 锁等待跨午夜;昨日未执行请求不能在新一天到账;客户端时区/时钟篡改无效 |
| 规则边界 | 规则变化/暂停/未发布;成功旧回执仍可读,未成功新请求拒绝 |
| 原子回滚 | 流水 insert、余额 update、commit 故障;无“加分成功但无流水” |
| 数据边界 | 负数、INT 上限、坏元数据、NULL 历史事件、非规范业务键不被误认 |
| 所有权与隐私 | 匿名/禁用/跨账户、流水游标和自由备注;只本人读写 |
| 老写方 | 后台伪造 score/level、legacy 方法/插件调用;不能覆盖新账本 |
| 页面状态 | 实际响应丢失、原键确认、换账号、登录恢复、加载/空/错、迟到回包 |
| 迁移和恢复 | 原表只读预检;新合成库apply4增量/重复0、完整备份/ACL/第二库恢复、旧数据摘要一致及精确清理已通过 |
当前后端可在规则未定稿时保持默认关闭。下一开发子项可实施积分页面真实读取、登录恢复及原键确认流程;规则未发布时签到不可用,不补造奖励。首阶段业务规则定稿、原表迁移和发布另行安排;邀请资格/奖励规则与关系方案留POINT-003,完整POINT-004后续推进。发布参数在确认前保持空值。
正式迁移、上线、真机/多设备、权益运营和真实业务验收后续按实际范围安排,不复用历史生产授权。本轮为源码、纯测试及显式隔离开发检查,不是生产签到到账或积分页面完成证据。