一、force_me 设计说明
<librose>/health.cpp
-------
const std::string thealth::health_dat_filename(bool force_me, time_t t) const
{
const struct tm* timeptr = localtime(&t);
if (timeptr == nullptr) {
// if t is too lareg, for error with ms unit, timeptr maybe nullptr.
return null_str;
}
const std::string& health_dir = force_me? me_health_dir_: curr_health_dir_;
char buf[512];
SDL_snprintf(buf, sizeof(buf), "%s/health%04d%02d%02d.dat",
health_dir.c_str(), 1900 + timeptr->tm_year, timeptr->tm_mon + 1, timeptr->tm_mday);
return buf;
}
1.1 为什么引入 force_me
背景
Launcher 的做操数据(healthYYYYMMDD.dat)有两类来源:
- 自己的数据 —— 存储在 saves/health/ 下,由本机跟练产生。
- 粉丝的数据 —— 存储在 fans/<uid>/health/ 下,由粉丝从手机导出后发给 UP 主。
看板界面(图表窗口)可以在“我”和多个粉丝之间切换。为了支持这种切换,代码中引入了一个运行时状态:
std::string curr_health_dir_; // 看板当前选中的目录 const std::string me_health_dir_; // 永远指向"我"的目录
curr_health_dir_ 会随着看板切换而改变;me_health_dir_ 则是常量,永远指向自己的目录。
问题
后台任务(例如“正在做操”)在运行时,需要始终读写自己的数据,不能受看板当前选择的影响。否则会出现:
- 看板正在看粉丝 A 的图表;
- 后台正在做操,写自己的实时数据;
- 如果直接使用 curr_health_dir_,数据会被错误地写到粉丝 A 的目录下。
解决方案
引入一个明确的开关 force_me,让调用者显式声明:
“这次读写必须走我自己的目录,不受看板当前选择影响。”
1.2 force_me 的功能
force_me 是一个 bool 参数,语义为:
是否强制使用 me_health_dir_(自己的目录),而忽略 curr_health_dir_(看板当前选中的目录)。
| 取值 | 使用目录 | 典型场景 |
| true | me_health_dir_ | 后台做操、实时写入自己的数据;任何“必须操作自己数据”的逻辑 |
| false | curr_health_dir_ | 看板渲染当前选中的用户(可能是自己,也可能是粉丝) |
关键点:
- force_me == true 时,完全不看 curr_health_dir_,哪怕看板正在看粉丝。
- force_me == false 时,走 curr_health_dir_,具体是谁的数据由外部状态决定。
- force_me == false 时,curr_health_dir_ 有可能等于 me_health_dir_(看板正在看自己),也有可能是粉丝目录。
所以 force_me 表达的是“是否绕过看板选择”,而不是“数据是不是我的”。 这就是它不叫 is_me 的原因——is_me 会让人误以为它描述的是数据归属,而实际上它描述的是目录选择策略。
1.3 核心原则:写操作默认走 true,读操作才可能需要 false
这是使用 force_me 时最重要的一条判断准则,理解它就掌握了整个设计:
原则上,UP 主不会修改粉丝发来的 healthxxx.dat。
所有写操作,默认就是写自己的数据,因此 force_me = true。
只有读操作,才需要考虑 force_me = false(看板当前选中的用户)。
为什么这样设计?
1. 粉丝发来的 .dat 是“只读快照”
- 它是粉丝手机上的原始记录,UP 主看的是这份快照。
- UP 主不该、也不需要去改它。改了会让粉丝本地数据和 UP 主看到的不一致。
- 所以“写粉丝的 .dat”不是制度性的常规行为,而是意外情况下的特殊操作。
2. 评语的写入路径是“粉丝侧”
- UP 主在图表上写评语,评语本身是UP 主产出的一条数据。
- 它通过私信/群发等方式发给粉丝,由粉丝自己的 App 把它融入粉丝本地的 healthxxx.dat。
- 对粉丝的 App 来说,这是写自己的数据,所以走 force_me = true。
- 对 UP 主的 Launcher 来说,评语只是被发出,不涉及写粉丝的 .dat。
3. 后台任务写的是自己的实时数据
- 正在做操时,姿态点、事件、TLV 历史都往自己的目录写。
- 这也是 force_me = true。
判断表
| 操作类型 | 默认 force_me | 说明 |
| 写自己的数据 | true | 后台做操、打卡累计、实时缓冲区 |
| 读自己的数据 | true 或 false | 明确要读自己时用 true;由看板当前选择决定时用 false |
| 读粉丝的数据 | false | 看板渲染粉丝图表 |
| 写粉丝的数据 | true(例外) | 不是制度性行为,未来若出现“UP 主修改粉丝打卡记录”这类功能,才需要特殊处理 |
1.4 force_me 的传播路径
force_me 不仅在 health_dat_filename 里使用,还会沿着调用链向上传递。设计原则是:
上层决定“这次操作是不是强制走自己”,把 force_me 原样传给下层,下层不再做二次判断。
典型调用链:
// 上层:后台做操,明确知道自己要写自己的数据
void do_workout_realtime() {
save_workout_data(true /* force_me */);
}
// 中层:根据 force_me 选择目录,并传递给下一层
void save_workout_data(bool force_me) {
// 可能做其他与目录无关的准备
std::string path = health_dat_filename(force_me, t);
// 可能继续传给其他函数
write_health_file(force_me, path);
}
// 下层:最终拼接路径
std::string health_dat_filename(bool force_me, time_t t) const {
const std::string& dir = force_me ? me_health_dir_ : curr_health_dir_;
// ...
}关键约束:
- force_me 必须原样传递,中途不得翻转、不得重新判断。
- 上层如果在某个分支里改用了 true,那么下游所有相关调用都要用 true,保持一致。
- 不允许出现“上层传 true,下层根据某些条件自行改成 false”的情况——这会破坏语义。
1.5 调用方必须注意的事项
1.5.1 明确自己在操作谁的数据
调用任何带 force_me 的函数前,先问自己:
“这次操作的数据来源,是必须是我自己的,还是由看板当前选择决定?”
- 后台任务、实时写入、打卡累计更新 → 必须用 true
- 看板渲染、图表展示、用户切换后刷新 → 用 false
1.5.2 线程约束
- 所有对 healthxxx.dat 的读写都发生在主线程。
- 单次调用内完成读或写,函数退出时操作已结束。
- me_health_dir_ 是 const,后台任务用 force_me == true 访问它是安全的。
- curr_health_dir_ 只在主线程修改,调用方需保证不跨线程修改它。
1.5.3 不要用 curr_health_dir_ 的当前值去推断 force_me
curr_health_dir_ 是一个运行时状态,它可能因为看板切换而随时改变。用它的当前值来决定 force_me 是危险的:
// 错误:用 curr_health_dir_ 推断 force_me bool force_me = (curr_health_dir_ == me_health_dir_); health_dat_filename(force_me, t);
这段代码看起来“聪明”,但它把“看板当前是不是在看自己”和“这次操作要不要强制走自己”混为一谈了。一旦看板切换,force_me 的结果就会变,后台任务的行为也会跟着变。
正确做法是:由调用者根据业务语义显式决定 force_me。
// 正确:后台任务明确知道自己要强制走自己 health_dat_filename(true, t); // 正确:看板渲染明确知道自己走当前选中 health_dat_filename(false, t);
1.5.4 如果未来支持多账号
force_me 的语义保持不变——“绕过当前选择,强制使用自己的目录”。如果未来支持多账号,me_health_dir_ 可能不再是全局常量,而是按当前账号派生。但调用方不需要感知这个变化,只需继续按业务语义传 true/false。
1.6 例外情况:写粉丝数据
未来可能出现非制度性的写粉丝数据场景,例如:
- UP 主手动修正某次粉丝做操的打卡记录;
- UP 主给粉丝的某次做操补一条本地备注(不是评语,而是本地备注);
- 平台层面的数据修复。
这些属于意外情况,不是常规流程。遇到这类需求时,不应该直接改 force_me 的默认值,而应该在调用处显式传 false 并加注释说明“这是一次例外操作”。
// 例外:UP 主手动修正粉丝 A 的某次打卡记录。 // 这不是制度性行为,仅在明确的修复场景下调用。 health_dat_filename(false /* force_me */, t);
注意: 这里的 false 意味着“走 curr_health_dir_”,因此调用前必须确保 curr_health_dir_ 确实指向目标粉丝的目录,且调用后不再被看板切换打断。
1.7 一句话总结
force_me == true 表示“强制使用自己的目录”,force_me == false 表示“使用看板当前选中的目录”。
原则上,写操作走 true(写自己的数据),读操作才可能需要 false(读当前选中的用户)。
UP 主不改粉丝的 .dat;评语是发给粉丝、由粉丝侧写入自己的文件。
未来若出现写粉丝数据的特殊需求,属于例外,需在调用处显式说明,不改变默认原则。