Commit Graph
68 Commits
Author SHA1 Message Date
laoyang e0562b8627 v24 第 5 轮:十条清单(天气填城市 / 输入即筛选 / 看板列=看板 / 账本盘点折线图 / 纪念日分类)
用户 2026-10-03 的十条,逐条落点:

1. 天气改为**个人设置里填城市**(每人一份):新增 app/Geo.php,内置 197 个
   地级市坐标(离线、逐条核对,生成脚本 _tooling/_fb_geo_gen.py)。填坐标仍
   作为内置表没有时的出口;老的家庭级坐标保留为兜底,升级当天天气不会凭空消失。
2. 所有"找个人"的地方**输入即筛选**:新增 assets/js/filter.js(纯前端显隐,
   不发请求、回车不刷新)+ app/PersonQuery.php 把服务端判据收敛成一份
   (名册 / 挑人 / 高级搜索共用,避免三处实现漂移)。
3. 名册列表:有昵称就不重复显示本名、行变矮、补齐手机/阴历阳历生日(带年份)/
   邮箱/工作单位;「清除」与「搜索」同款同尺寸。
4/5. 日程:撤掉左侧日历本栏改满宽,功能(显隐/颜色/导入/默认视图)全收进
   右侧 ☰;**色块高度改为按起止时长**(旧实现一律 30 分钟,三小时的会跟
   十五分钟的同高);点日历空白处即建日程,开始时间=点的那一格。
6/10. 资源:搜索提到最上;按"最近改动/最新新增"排序,默认只显示最新 10 条并
   给「显示全部」出口。
7. 任务:看板的**列=用户自定义的看板**(不再是写死的待办/进行中),
   拖动/左右键=换看板(改 board_id,不动 status)。
8. 财务:概览在上账目在下、记一笔改右侧按钮、账本设置(共享/改名/新建/删除)
   收进 ☰、默认账本名「xxx的账本」、"挂在谁名下"默认自己且用标准选人框;
   **新增盘点**(新增 ledger_entry_snapshots 只增不改的历史表,旧值不被清空)
   + 上面画金额变化折线图(app/chart.php 服务端画 SVG,零依赖)。
9. 纪念日:手动添加的与名册生日派生的**分两块**、手动的在上;右侧设置可增删
   分类(家庭级,删分类不强改已用的历史)。

本轮另外修掉/加固的四类问题(都由新增的静态或运行时守卫兜住):
- index.php 少写 2 个 use 导入(Pinyin / UserSetting)→ 名册与个人设置
  返回 200 但正文是 Fatal;顺带修了一处早已存在的 Attachment 未导入。
  → 新增 _fb_import_check.py(1f 段)。
- 一次不精确替换弄丢了 app/Calendar.php 开头的 `<?php`,整份源码被当 HTML
  吐给浏览器,而 `php -l` 报"无语法错误"(纯文本输出确实合法)。
  → 新增 _fb_php_open_check.py(1e 段)。
- 模板片段 unset 了宿主模板的变量(账本片段 unset($canWrite))→ 正文里一片
  Undefined variable 而 HTTP 仍 200。同 _datetime.php 早年 $t 的坑。
  → 新增 _fb_fragment_scope_check.py(1g 段)+ _fb_batch_e.py 的 26 页无告警巡检。
- 窄屏下页头动作区不换行 → 周视图横向溢出且按钮被挤成三种高度(用户提过三次
  "按钮要同高")→ .page-head .actions 加 flex-wrap。

验证战绩(全绿):
  e2e 773/0 · 第 5 轮专项 91/0 · 结构升级 126/0 · 几何 428/0 · 移动端 684/0 ·
  交互 97/0 · 家谱 26/0 · 按钮 60/0;三轮反向验证各 3/3 抓到,还原后复绿。
线上:schema v14、指纹一致(9df6c9fccc6796db)、snapshots 表已建、197 城、
  8 个关键文件 md5 与本地逐字节一致、app/ 403 无源码泄漏、错误日志无新增。
2026-10-03 15:01:38 +08:00
laoyang dbcf0c1bcd 根治「登录后 500」:结构升级改成按源码指纹触发,不再依赖人记得改版本号
## 故障与根因
用户报「首页能开,登录后就 500」。上一轮加的错误自记录抓到了确切原因:

    SQLSTATE[42S22] Unknown column 'd.is_public'
    app/Calendar.php:650  ← Calendar::month() 的旅程段

线上 trip_details 缺 is_public 列、calendar_event_skips 表不存在,
但 settings.schema_version 是 12 —— 而 index.php 的判断是:

    if ($have !== FB_SCHEMA_VERSION) { Schema::ensure(); ... }

线上 12 == 代码 12 → 判定「已是最新」→ **整段 v13 升级被跳过**。
连续两批提交(8f80f3e、02f5d07)往 ensure() 里加了 8 个新列 / 2 张新表,
但**都没改 Schema::VERSION** —— 而"忘了改版本号"没有任何检查拦得住:
不是代码错,是流程漏。

## 根治(换判据,而不是"这次记得改")
- Schema::fingerprint():拿 **Schema.php 文件内容**的哈希当判据。
  只要 ensure() 改过(哪怕只加一行 addColumn),指纹就变,下一次请求必然重跑。
- Schema::needsUpgrade() / markUpgraded():一读一写,落 settings。
- index.php 的门槛改成指纹;install.php 装完也盖指纹章。
- ⚠️ 指纹**只用内容、不含 mtime**:部署(tar 解包)会重写 mtime,
  带上它等于每次部署都白跑一遍升级。

## 顺带修掉的三个真问题
1. **「老日程时刻并进」不是幂等的**:并完没清 start_time,重跑会把陈旧时刻
   盖回去 —— 等于**悄悄撤销用户后来改的时间**。现在并完就置 NULL。
2. **测试脚本的 check() 无条件打印 extra**(升级测试里):extra 写的是
   "为什么没通过",成功时也打印,读日志的人会以为出错(本轮确实误判过一次)。
3. **断言里写死版本号**(e2e / 升级测试 / 部署 chk 共 5 处):版本号是代码常量,
   写死后每轮假红。改成"与代码常量一致" + "指纹已记",
   后者才是真正的哨兵(版本号对但升级没跑的正是上次事故的形态)。

## 防复发
- 部署脚本新增 1d 段:只要有人把门槛改回版本号、或指纹里掺进 mtime,**拒绝部署**。
- e2e / 升级测试新增哨兵:`schema_fingerprint` 为空即失败
  (= 自动升级机制没生效,下次加列还会重演)。

## 验证
- 本地模拟「库停在旧结构」:退掉 is_public、删表 → 发一个请求 → 自动补齐、
  日历页从 524 字节含 Fatal 变成 36597 字节无 Fatal。
- 反向验证:把门槛改回版本号(版本号伪装成已最新)→ 必现 500,因果链确认。
- 线上:部署后一个请求即自动补齐(is_public ✓ / calendar_event_skips ✓ /
  指纹已记);登录态 16 个页面全部 200、零异常。
- 端到端 773 项 / 升级 126 项全过;几何 368 / 交互 97 / 移动端 516 /
  家谱 26 / 版面 10 全绿。
2026-10-03 12:46:44 +08:00
laoyang 02f5d072a4 家谱卡片重做 / 首页控制台 / 名册改列表 / 重复日程与旅程公开
用户 2026-10-03 提的四条:

① 家谱卡片重做
   · 去掉「⇄ 姻亲」按钮,改成**每个人**卡片右下角都有「以 TA 为中心」
     (点了以他重画一张家谱,涵盖原来的姻亲场景)。
   · 头像外框改**长方形**(96×72,宽度贴合头像),object-fit: contain 不裁切,
     姓名/称呼在头像下方。
   · 修「点右下角按钮误进成员页」:卡片不再整张是 <a>,
     只有姓名是链接,右下角是独立的 <button>(物理上分开,不抢点击)。

② 家谱文案与按钮清理
   · 去掉副标题与下方图例说明;整块去掉「看全部 / 回到我 / 回到我的家谱」
     (顺带消掉"出现两个回到我的家谱")。镜头由库自动 fit,拖拽/滚轮即可。

③ 首页控制台
   · 标题下显示个人 slogan;去掉右侧三个统计;「我的资料/我的家谱」换成图标按钮;
     去掉与导航条重复的「名册」按钮。
   · 去掉「接下来的日子」(生日归到"未来七天"里,不再重复)。
   · 新增统计信息块:按时段随机问候 + 今日天气(Open-Meteo,免 key,可留空不显示)
     + 近七天日程/待办/逾期数。
   · 新增各模块小结卡(任务/日程/纪念日/观影/旅程/财务/名册/家谱/资源)。
   · 个人设置新增「首页上的模块」:可勾选与拖动排序(每人一份,存 user_settings)。
   · 保留「往年的今天」。

④ 名册改列表
   · 卡片网格 → **竖向列表**:左头像、中间姓名+与我的关系+手机号+生日。
   · 修拖动「只能往后不能往前」:落点改为显式记录"插到谁前面/后面",
     并排除被拖元素自身。
   · 去掉那句拖动提示(能力保留)。
   · 搜索新增**姓名首字母**检索(y / yy / yyq 都命中「杨永全」):
     新 app/Pinyin.php,intl 优先 + 内置常用字表兜底(本地无 intl 也能测);
     纯字母/数字查询词不走 SQL LIKE(拼音与加密手机号在库里都比不了)。

附带上两轮的收尾:
   · 菜单点外部自动关闭(全局事件委托,覆盖所有 details.menu)。
   · 旅程 is_public:列表与日历只显示公开的;详情页不拦(不丢数据)。
   · 重复日程:新 app/Repeat.php(RRULE 解析/生成/展开,含"工作日"),
     新表 calendar_event_skips(RFC 的 EXDATE);日历按规则展开;
     编辑支持三档「全部 / 这一次及以后 / 仅此一次」。
   · 结束时间可添加/去掉,并随开始时间联动。

验证:端到端 772 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 26、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 11:30:48 +08:00
laoyang 8f80f3e835 v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)
用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 08:18:05 +08:00
laoyang 72dc6c0006 家谱关系推导:把「只记了一头」的边补成双向(修用户报的 4 处少人)
用户指出 4 个具体错误,逐条核对线上真实数据后确认**全部是算法缺陷**:
  ① 杨永华只有一位家长(母亲那边整条线缺失)
  ② 洪博为的孩子是空的 —— 她是杨永全的配偶,杨永全的孩子就是她的
  ③ 洪勇、武竹芳的配偶都是空的 —— 两人明明是同一个孩子的父母
  ④ 杨景焕 / 杜浩洋 / 杜若桐 都只有一位家长

根因:本系统的设计是「只存相邻两代之间的边,其余全部推导」(见 Kinship 头注释),
但家人手工录入时只会填**最直接的那一条**:
  · 给几个孩子都填了「父亲 = 某人」,从没点过「母亲」→ 孩子只有一位家长;
  · 填了孩子父母两位,却忘了把这两位再标一次「夫妻」→ 两位家长各自孤零零。
而 graph() 只做了「parent 边反推 children」的**单向**推导,缺了两条互补推导。

修复(都在 graph() 里,内存视图,不动 relations 表):
  推导① 同一个孩子的两位家长 ⇒ 认定他们是夫妻
  推导② 孩子只有一位家长、且该家长**恰好只有一位配偶** ⇒ 把配偶补成另一位家长
         (有多个配偶时不猜;迭代到稳定,最多 4 轮)

配套修复(实测发现的回归,必须一起上):
  推导① 会把「已解除的配偶」凭共同孩子**配回去** —— 用户点"解除配偶"刷新又出现。
  解法:解除配偶时把这一对记进 settings 的 `kin_undone_pairs`(不动表结构),
       推导①跳过这些对;重新 addSpouse 时抹掉标记。
  验证:初始有配偶 → 解除后为空(不再被推回)→ 重新加回恢复。

验证:
· _fc_verify_derive.py(新):用**线上真实那 10 条边**做夹具,逐条断言用户报的 4 处
  + 11 条「不许过度推导」守卫(祖父母不被推成夫妻、孩子家长不多不少、无自环…)
  全绿;反向验证 2 处注入(关推导①/关推导②)各变红 6/8 条且退出码非 0。
· e2e 549/0、家谱可视化 24/0、家谱版面 10/0、桌面几何 368/0、移动端 516/0、
  交互 97/0、升级专测 123/0。
· **线上真机验证**:修复后重新取线上 data-datum,4 处全部正确 ——
  洪博为子女=杨景焕、洪勇⇄武竹芳、杨景焕双亲=杨永全+洪博为、
  杜浩洋/杜若桐双亲=杨永华+杜元青、杨永华双亲=杨培振+刘巧莲。
· 部署脚本加 2 条断言锁住推导逻辑。
2026-10-02 22:08:13 +08:00
laoyang 80f5c4f0fc v15: 家谱重写 —— 以我为中心检索全体亲属 + 自己排版画线
用户 2026-10-02 的反馈(原话):
  「显示不齐全,应该显示本人的上一代,这一代,和下一代的所有人…以本人为中心去搜索,
    只要有关系的,都检索出来进行展示」
  「夫妻之间有一条横线…这对夫妻下面所有的子女,都应该从夫妻之间的这个横线出来,
    而不是从这对夫妻的其中一个出来,这样可能会出现一个 bug,就是如果从一个出来,
    例如从父亲出来,母亲登录进去竟然看不到自己的孩子」
  「数据仍然不全,差很多」

根因(实测,不是猜):第三方库 family-chart **只画了 20 人里的 9 人**。
  _tooling/_fb_kin_cover_probe.py 的硬数据:
    画出的:祖父/祖母/父亲/母亲/我/妻子/哥哥/儿子/女儿(9)
    没画的:伯父、伯母、岳父、岳母、嫂子、堂兄、堂嫂、舅子、侄子、堂侄、外甥(11)
  库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」,
  **横向亲戚(伯父那一支)与姻亲旁支(妻子的兄弟)永远进不来** ——
  换树根也救不了(换到最高祖先,"妻子的兄弟"照样不在树里)。
  「母亲看不到孩子」还有第二层原因:孩子只登记了父亲时,母亲那一侧根本没有边。

改法(不再依赖库的树算法):
  · app/Kinship::familyMap() —— 以「我」为中心的两轮贯穿检索,带 seen 防环:
      父母(-1 代)/ 子女(+1 代)/ 配偶(同代)/ 每一层的配偶
      / 配偶的父母与子女(姻亲旁支由此进来)/ 兄弟姐妹的子女(侄甥)
    → 上一代、这一代、下一代的**所有有关系的人**都在名单里。
  · app/Kinship::householdChildren() —— 子女按**父母所在的户**归堆,
    而不是按"某个人的 children"。于是孩子只登记了一位父母时,
    照样挂到这对夫妻的户上(语义:这孩子是我们俩的)。
  · templates/kin/tree.php —— 自己排版:每代一行、按「户」水平排开;
    夫妻两张卡之间画短横线;子女一律从**这条横线的中点**垂下来
    (x = (左卡中心 + 右卡中心) / 2,与"谁登录"无关)。
  · 拖拽/缩放保留(用户明确要),改用原生 d3.zoom;不再引 family-chart。

验证(只跑这一个模块,按用户要求):
  · _tooling/_fb_kin_v15.py —— 22 项全过:
      ✔ 20/20 全员画出(== 从「我」出发可达的人数)
      ✔ 7 对夫妻都有横线;7 个有子女的户**下垂线全部落在横线中点**
      ✔ 用户点名的场景一:删掉「母亲→儿子」这条边(模拟只登记父亲)后,
        下垂线**仍然**从夫妻横线中点出来,孩子仍在图上
      ✔ 用户点名的场景二:给母亲开账号、**用她的手机号登录**,
        家谱里能看到自己的孩子,且与父亲视角画的是同一批人
      ✔ 截图存档(全图 / 母亲视角 / 窄屏)
  · _tooling/_fb_kin_v15_falsify.py —— 反向验证 2/2 抓住:
      注入「从其中一张卡出来」→「从横线中点出来」变红
      注入「只走直系血亲」→「每个人都被画出来」变红
  · e2e 550 条全过(家谱段改到 v15 的判据上)

顺带修的既有问题:
  · app/Kinship.php 老库升级回填按实际存在的列动态拼(缺列不再 1054 炸断升级)
  · _fb_e2e.py 家谱断言改到 kin_v15_nodes(旧解析器在新页面上只会解析出空)
2026-10-02 10:26:14 +08:00
laoyang 71a7ec35a0 v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。

观影(1-4)
- 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法)
- 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库),
  瀑布流卡片也按人显示;五处观影模板全部改用它
- 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名)
- 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块)

家谱(5)
- 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹",
  根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人)
- focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方
  —— 修掉"母亲登录看不到孩子"
- 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势)

统一事件原语(6-9)
- 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。
  复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留)
- 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本,
  内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变);
  没有具体时分 = 全天;月视图参照群晖 Calendar
- 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写,
  默认折叠、可搜标题与正文
- 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期)
- 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算

模块化(10-11)
- App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现)
- 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来)
- 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色

其他(12-13)
- 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己)
- 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。
  家人卡片从首页撤走(看人去名册)

验证(全部通过)
- e2e 542 条(新增 4 模块端到端 60+ 条)
- 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123
- SQL 列名静态校验 0 处可疑(对着真实表结构)
- 反向验证 18 条新注入全部抓住

顺带修掉的既有问题
- 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级)
- 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备
- 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
2026-10-02 09:28:07 +08:00
laoyang e9b3b8666b v11: 家谱按户排布(夫妻并排/长幼排序/父母括号/姻亲);观影设置下拉收拢影院·搜索·统计·年度报告;观影人头像
家谱(用户第 2 点,重写模板与 Kinship::tree):
- 同代聚成「夫妻单元」(户):血亲当主位、配偶并排在其右,兄弟_姐妹按长幼(长子左)
- 血亲可达性判「姻亲」:只走 parent/child 边从「我」可达者为本家血亲,妻子的父母
  走不到 → 标「姻亲」并排到本代末尾(旧判据「上无父母且下无子女」对岳父母恒为假,是真 bug)
- 父母括号连线:横杆盖住父母列到其子女各列,竖线落父母列中心、每子女列一道下线
- 称呼按长幼定死:姐夫/妹夫、嫂子/弟媳、大/小舅子、大/小姨子、大伯子/小叔子、
  大姑子/小姑子、侄子/侄女、外甥/外甥女、堂/表
- 列宽地板 minmax(min-content,1fr):原 minmax(0,1fr) 在窄屏+户数多时把列压到卡片
  (96px)以下 → 同代卡片互相重叠;.tnode 加 flex:0 0 auto 防被 flex 压扁

观影(用户第 3~5 点):
- 顶栏删「影院」入口(影院是观影模块的子功能);观影列表右侧加「⚙ 设置」下拉,
  内含 影院管理 / 高级搜索 / 统计 / 年度报告(后三者从 MovieLog 迁入,
  app/MovieStats.php + 三个只读路由,MySQL 方言;图表全用纯 CSS,不引 CDN)
- 评价区删除「我」的长条徽标(被 flex column stretch 拉成整行宽的丑色带)
- 观影人弹窗列表与已选 chips 带头像;syncChips 点名取 .att-name/.att-av
  (旧写法取「第一个 span」,v11 起那是隐藏占位,chip 上会没名字)

验证:e2e 432/0、桌面几何 268/0、移动几何 464/0、交互几何 97/0、
静态空跑 199/0、反向验证 v11 十条 10抓/0漏。本轮无 DB 变更(Schema 仍 v10)。
移动几何抓出两个真溢出并已修:高级搜索页 select 长选项撑破 .field、
原生 date 输入撑破 .date-range(360/375px)。
2026-10-01 12:11:43 +08:00
laoyang 2c36335c3e v10: 修影院编辑回填布尔化 bug;评分改 10 星制;观影人改弹窗+拼音首字母联想;豆瓣自动补充迁移(movie_details.douban_meta,schema v10) 2026-10-01 10:21:45 +08:00
laoyang 746f02581e v9: 观影对齐 MovieLog —— 瀑布流/影院管理/评价评论/起止时间
- events 起止时间精确到秒(starts_at DATE→DATETIME + ends_at),_datetime.php 复用
- 影院独立成表(cinemas,地方/名称/地址/主题色 + 50 色板),movie_details 归一为 cinema_id
- 评价模型 MovieLog 同款:movie_reviews 每账号每片一条(1-10 分+正文+感受 key),
  movie_review_comments 两级平铺评论(回复的回复规范化到顶层、跨评价回复拦截)
- 感受目录迁到 MovieReview::CATALOG(12 种,Emoji 11.0 老码位)
- 观影列表改瀑布流(CSS grid 海报墙),详情页五大块 + 灯箱 + 评价区
- 升级路径:v8 老数据自动归一(影院三段按 (family,place,name) 去重建行、
  评价回填、中文感受反查英文 key),回填全部幂等;老库补 cinema_id 列+索引+外键
- 修三 bug:建表顺序(cinemas 必须在 movie_details 前,否则全新安装 1215)、
  layout 导航用键名当路由(影院链接指向 r=cinemas)、
  _datetime.php 分钟循环变量 $m 覆盖宿主模板的观影记录(编辑页简介/备注回填被清空)
2026-10-01 00:12:04 +08:00
laoyang ff353b1eee v8:观影落地(MovieLog 整体迁入统一事件原语)+ 三处 UI 撤除
- Schema v8:events / movie_details / event_attendees / event_photos 四张表,
  uk_event_source(family_id, source_ref) 作迁移幂等锚
- Movie 模型:时间线/详情/增删改、同行人整组替换、照片逐张管理、感受英→中映射
- ImageSlot 新增 poster(2:3) / ticket(free) / photo(free) 三槽位;
  free 槽位不裁剪、整图等比缩进框(只缩不放大)
- OssDriver::copyObject:同桶 CopyObject(手写 V1 签名,x-oss-* 头按字典序进签名串)
- 路由:movies 时间线/详情/新增/编辑/删除/补照片/删照片;同行人按家庭过滤;
  访客只读(requireWrite)
- UI 撤除:首页「我的一句话」、家谱页「补充我的亲属关系 / 看整个名册」两个按钮;
  顶栏加「观影」入口
- 修潜伏 bug:ImageSlot::parseRatio 的正则用 / 作分隔符,字符类里的 / 把模式
  提前截断(Unknown modifier ']')—— ratio() 只在上传 POST 里被调到,一直没被观察到
- 线上数据迁移(临时 token 门禁页,用完即删):MovieLog 219 条观影 → events,
  14 位观影人新增进名册(带 MovieLog 头像),431 个 OSS 对象同桶拷贝
  (movielog/uploads/<dir>/x → family/<dir>/x),review/feelings/影院三段/照片全量搬
2026-09-30 20:22:46 +08:00
laoyang 809443d429 v7:家谱树与名册三处口径统一(树上的人自动互相可见)
用户反馈:我把一个人设成配偶、又设了我们的孩子,配偶登录后首页能看到
孩子、家谱也能看到,名册里却看不到——没分享的人却在家谱上看得到,
口径打架。

口径由用户拍板:**以每个人为中心上下三代那棵家谱树(= 首页「我的家人」
同一棵)里的人,自动进他名册(source=kin,默认归公共「家人」组);
树外的人三处都不可见。**

- PersonRoster::syncKinForFamily():整树同步(补缺、撤多余;已存在的
  self/shared/seeded 行绝不碰,用户改过的分组/顺序保留)。幂等。
- 同步挂在事件上:建边 / 解边 / 开通账号 / 结构升级回填。
- Relations::addParent/addSpouse 建边后同步;remove/detach 解边后同步。
- 名册不许单独移走 kin 来源的人(服务端拦截 + 人话提示 + 卡片隐藏
  移走按钮改提示语):他在首页/家谱照样看得见,单藏名册只会回到口径
  打架;想不再看到他,解除关系即自动移出。
- 名册卡片给 kin 来源标「家谱」徽标(来历可辨)。
- Schema v7:widenRosterSourceEnum(⚠️ CREATE TABLE IF NOT EXISTS 不会
  更新已存在表的枚举,v6 老库不扩宽则 strict 模式下插 kin 整条被拒、
  且被容错吞掉——e2e 的 v6 旧库当场抓出)+ 按已有关系回填 kin 条目。

验证:e2e 309/0、升级专测 82/0、部署静态空跑 78/0、桌面几何 0 失败、
交互 97/0、移动几何 180/0;线上探针 ver=7、enum 含 kin、无升级告警。
2026-09-30 16:12:28 +08:00
laoyang f7609a3ed9 v6:名册联系信息对齐 vCard + 头像与裁剪四项修正
- ① 头像上传失败(超限/非图片)不再挡保存,改为 flash 提示「已加进名册。头像没有保存:…」
  ——这是「添加不了」的真根因(错误被塞进 errors.avatar 把整条新增打回)
- ② 表单顺序:生日整块下移(身份证能读出生日时无需常改)
- ③ 裁剪缩放语义反转:框在屏幕上的位置与大小不动、动它底下的图;
  落库归一化矩形随倍率缩小(放大=同一块区域看得更清楚)
- ④ 取消应用层 5MB 兆数上限;按像素动态上调 memory_limit(px*6+64M,封顶 768M);
  头像输出恒为 512x512
- ⑤ 成员信息对齐 vCard:person_contacts 表(phone/email/address/url 可多行,
  value 加密落库)+ persons.org(工作单位)/title(职务职称);均可留空

Schema v5→v6:person_contacts 建表 + persons.org/title 加列
2026-09-30 15:02:27 +08:00
laoyang ed38a40c08 名册每人一份 · 分组两层 · 拖动排序 · 表单合一 · 裁剪可缩放
按用户十点反馈逐条落地(v5,Schema::VERSION = 5):

- 名册改成每人一份:新表 person_roster(owner_id, person_id, group_id,
  sort_order, source)。默认只看「我添加的 + 别人分享给我的」;
  source='seeded' 是升级时从老 persons.group_id 播进来的(先播数据再删列)。
- 排序不再手填:名册页直接拖动卡片(整版提交,一次 applyLayout 落库),
  无 JS 时卡片上的「调整分组」表单照旧可用。
- 分组分两层:管理员在「系统设置」维护公共分组(owner_id=0),
  用户在「个人设置」加自己的分组;显示顺序各人各定、互不影响。
- 名册可共享:把名片分享给别的用户(people/share);添加成员时若系统里
  已有这个人,可直接复用(people/pick),不必重复录入。
- 「编辑成员」与「改我自己的资料」合并为同一张表(templates/people/form.php),
  两个入口 href 完全相同,h1 按改谁切换文案;个人设置页只留
  资料入口 + 称呼 + 我的分组三块,删除 templates/settings/_profile.php。
- 系统设置收进头像下拉,顶栏只剩 首页/家谱/名册。
- 裁剪器加缩放(滑块 1-4x、Ctrl+滚轮、还原):缩放只改视图大小,
  落库的归一化矩形保持不变。
- 权限:管理员可以改任何人的权限(含自己),只拦「最后一位管理员」被降级。
- 去掉首页「我的一句话」后的链接、家谱说明段、以及「还没归到任何分组」提示。

迁移安全:数据回填(PersonRoster::seedFromPersons)先于删列执行、且不进
tryStep 容错;失败步骤记入 settings.schema_upgrade_warning 并在系统设置页提示。
线上已验证:schema_upgrade_warning 为空,person_roster/person_groups.owner_id
就位,persons.group_id / sort_order 已删。

验证:php -l 48 文件;本地 e2e 277/0、老库升级 81/0、桌面几何 108/0、
移动端几何 180/0、交互 96/0;反向验证 38 条注入 + 2 条缩放注入全部命中;
部署静态断言 53/0,站点自检 7/7(--resolve 锁本机走 443,探针真的进 PHP)。
2026-09-30 13:24:26 +08:00
laoyang 1c460a0e83 图片可裁剪 · 名册按关系分组 · 个人设置独立 · 顶栏下拉菜单
① 上传图片一律可裁剪,比例由图位决定
   新增 app/ImageSlot.php 统一配置(头像 1:1/512×512;题图 16:9 只留注释,
   将来启用只加一行)。前端只挑区域、写回归一化矩形 "x,y,w,h",真正裁剪在服务端
   Upload::fitRegion():夹回图内 → 在框内居中收缩到目标比例(不外扩)→ 钉住 sx+sw ≤ w。
   比例解析单独写 parseRatioAttr():parseFloat('16:9') 得 16,会把题图裁成细长条且不报错。

② 名册按关系分组,顺序每人可自定
   分类全家共用(person_groups,另一个人可以自己加分类);「自己的」只体现在顺序上
   (user_settings.group_order,每人一份)。persons.group_id 故意不加外键(老库只能
   ADD COLUMN),完整性由 PersonGroup 保证:删分类把人置回未分类,人不会从名册消失。

③ 设置分两级:系统设置 / 个人设置
   个人设置拆成 templates/settings/me.php + _profile.php + _nicknames.php,
   取代 templates/people/me.php(旧路由与旧表单仍兼容)。

④ 右上角「姓名/头像 + 下拉菜单」
   原来是两个裸链接,窄屏会被 CSS 藏掉导致改密码/退出不可达;改成 <details> 零 JS 菜单
   (app.js 只补「点外面 / Esc」)。菜单项:个人设置 / 系统设置(管理员)/ 修改密码 / 退出登录。

Schema 版本 3 → 4;安装与老库升级共用 PersonGroup::backfillMissing() 一条回填路径。

验证:php -l 全量;e2e 202/0(新增「关系分组」与「图片裁剪」两节);老库升级 49/0;
桌面几何 108/0;移动端几何 192/0(360/375/390/414);交互 76/0;
反向验证 31/31(逐条把修复改回旧写法,指定的目标断言必须变红);
部署后本地↔线上 72 个文件 sha256 逐字节一致。
2026-09-30 11:06:22 +08:00
laoyang 7e9b4520c0 登录居中 · 个人 slogan · 三种生日 · 称呼视角化 · 附件统一走代理
按九点反馈一次性改造:

1. 登录页「图标 + 用手机号登录你的家书」包进 .auth-head 并真正居中
   (以前只加了个 class 名,没有样式,看着像做了其实没做)。

2. slogan 改成两段式:系统级 slogan 作兜底,每个人可在自己的资料里写一个,
   填了就显示自己的、留空就回落到系统那句。settings 里「家庭名称」字段去掉
   (只在安装时归档,不参与展示)。

3. 三个生日:身份证生日(从证件号截取,永不改动)/真实阳历/农历。
   同一人填了其一即自动推出另一个(靠 birthday_source 指明填的是哪一侧,
   不依赖 JS)。三者落在同一天时合并成一条提醒,否则分别提醒。
   农历表不是照抄流传的那份 lunarInfo(它在 5 个年份上是错的)——
   用五个独立实现按 7.3 万天多数表决重新推导,再要求 100% 重现。

4. 称呼改成「视角属性」:nicknames(family_id, viewer_id, target_id, nickname)。
   「小明」不是某个人的属性——同一个人被不同人叫法不同,所以是「谁看」的属性。
   旧的 persons.nickname 由 Nickname::migrateLegacy() 搬到新表,只搬一次。

5. 点自己的头像/名字直接进「我的资料」,不必再绕
   「名册 → 找到自己 → 编辑资料」三步。

6. 附件(头像与其它上传)一律走 media.php 代理,不再直连 uploads/ 或云端地址:
   可读性由登录态决定而不是「知道文件名」,Content-Type/缓存/下载头由服务端统一给。
   uploads/ 整个目录 deny(含历史遗留备份文件),并有真实探针验证。

7. OSS 设置页加「连通性测试」(本地 / 云端两个按钮),逐步给出结论与耗时。

8. 生日输入改用「原生日期框 + 24 小时制时:分:秒 三个下拉」:
   datetime-local 的格式由浏览器 UI 语言决定(中文环境渲染成「下午 03:30」,
   没有秒、还容易把上下午填反)。点输入框任意一处都能弹出选择器(showPicker)。

9. 结构版本号收敛成 Schema::VERSION 一处,安装时即盖章,避免首次请求白跑升级。

验证(全部为「先注入 bug 证伪、再跑通过」):
  php -l 42 个文件;e2e 149 条;老库升级专测 37 条;反向验证 23 个注入全被抓;
  桌面几何 108 条(768/1280/1920/2560);移动端真机几何 212 条(360/375/390/414);
  交互检查 49 条(生日整框可点、登录居中)。
2026-09-30 09:44:55 +08:00
laoyang 1669ab48d0 五项家庭化改造:以个人为中心 · 家谱 · 三种角色 · 附件存储可配置 · 独立页样式
1. 独立页(登录/安装/错误)套上 HTML 骨架 —— 修「首页为什么没有样式」:
   新增 templates/layout-bare.php,View::render 对独立页也走完整 <head> + CSS。
2. 以个人为中心:Kinship::label() 现算称呼、Auth::homeTitle() 出「X的家书」;
   同一批人在不同登录者眼里自动变成公公婆婆 / 岳父岳母(不存一次性称谓)。
3. 附件存储可配置:Storage 门面(local / 阿里云 OSS),库里只存相对键,
   云端失败自动回退本地;设置页可选驱动并填 OSS 参数(照 MovieLog 的方案)。
4. 三种角色:admin / member / guest,访客只读、写操作一律 403;
   accounts.role 枚举扩容,老库靠 Schema::ensure() 自动升级。
5. 家谱:只存相邻两代的边(parent / spouse),其余称呼全部推导 ——
   兄弟姐妹不存边(共同父母 + birth_rank 排行推长子/次女、哥哥/妹妹),
   认不清的人用虚拟人物 is_placeholder 占位,关系天然双向。

顺带修掉本地 e2e 挖出的 8 个线上级 bug(性别写死、长幼方向反、关系方向静默写反、
birth_rank 不从表单取回、隐式可空参数致响应头失效、无文件字段被误判为上传失败、
layout 变量撞车覆盖 $me、世代 0 显示成「下 0 代」)。

验证:kinship 单测 37/0 · HTTP e2e 81/0 · 老库升级专测 14/0 ·
反向验证 11/11(把每个修复改回旧写法,对应断言都会变红)。
2026-09-30 08:13:07 +08:00
laoyang 8eb19267bc 阶段 0 完整版:名册 + 账号 + 安装向导 + 设置(宣纸主题)
这一版把「家」的地基铺完:

功能
- 安装向导 install.php:建四张表 → 建家庭 → 建第一人(兼管理员),装完自删;
  「是否已装」以数据库四张表为准,不以文件是否存在为准
- 家庭名册:成员增删改查、头像上传(居中裁方 512×512 转 JPEG、finfo 校验、
  随机文件名)、生日自动算年龄与倒数天数
- 敏感信息加密存储:手机号双写(SHA-256 哈希用于登录 + AES-256-CBC 密文用于显示)、
  身份证只存密文;页面上永远只显示脱敏后的 138****8000 / 前 6 后 4
- 账号体系:人与账号分离(孩子可入名册但无需密码)、开通/停用/角色、
  防爆破(5 次失败锁 60 秒)、修改密码、最后一个管理员不可移除
- 设置页:家庭名称与座右铭、加密密钥状态
- 宣纸主题 UI:砖红取自图标,扁平、留白充足;移动端顶栏收紧不换行

安全
- 所有 POST 走 CSRF;输出全量 e() 转义;uploads/ 禁执行脚本
- app/ 与 templates/ 加 .htaccess 整体拒绝直连(直接请求模板会 Fatal 并
  把服务器绝对路径打在页面上 —— 这是线上真实抓到过的问题)
- 已安装的 install.php 返回 404;display_errors 线上关闭

工程
- 无框架无 Composer,query-string 路由(index.php?r=模块/动作),不依赖 rewrite
- 静态资源带 mtime+size 版本号,改版后浏览器不会吃旧缓存
- 配套部署与冒烟(_tooling/,不入库):部署脚本容忍 tar 对非属主目录的
  元数据回填失败;冒烟断言「状态码 + 正文无 PHP 错误 + </html> 收尾 +
  无绝对路径泄露 + 断言总数下限」—— 只看状态码会把「200 但正文中间
  Fatal」当成通过,这一版在线上真实抓到过一次
2026-09-30 00:49:53 +08:00