Commit Graph
11 Commits
Author SHA1 Message Date
laoyang 388d5dc212 首页改成「旅行地图 / 活动列表」两个标签页(批 16)
原来首页是一屏卡片网格,一次旅行一张卡,看不出「去过哪儿」。改成:

- **默认「🗺️ 旅行地图」**:一张大地图(`clamp(520px, 78vh, 920px)`),
  **一次旅行一枚图标**(有封面放封面、没有放 📍,下面挂一行活动名),
  点图标直接进那次旅行的详情页。位置 = 那次旅行所有坐标点的**重心**
  (执行节点优先、计划节点兜底,取消的不算)。
- **另一个标签「📋 活动列表」**:每行一条 —— 左边封面,右边
  标题 + 简述 + **主要参与人**(头像/名字,超过 5 位显示「等 N 人」)+
  去了几个地方(地名去重)+ 经历了几天(自然日)+ 几个节点。
- 只有一次旅行(或几次都在同城)时 `setFitView` 会一路放大到街道级 ——
  那是导航视角不是"旅行足迹"视角,用 `MAX_ZOOM = 11` 压住。
- 一次旅行都没落坐标的家庭:不出标签条,退化成原来的单页列表。
- 图标下面挂了活动名,整块是 56×64 **矩形**,判重叠就按矩形;
  详情页那种纯圆标才按中心距 —— 形状不同判定不同,混用会误判。

数据由 `ActivityController::collectStats()` 用几条 IN 查询**一次聚合**
(不做逐活动的 N+1):节点明细 / 参与人(按参与节点数降序)/ 时间跨度 / 地点去重。

验证:新增真浏览器用例 `_page16.php` + `_map16.js`(首页 **20 项**),
夹具覆盖「两个重心只差几公里的同城旅行」(防重合回归)+「完全没坐标的活动」
(不上图但列表里要有)。首轮抓到两件事:① 断言拿了被截断的标题做匹配;
② 夹具里只有两个点,`fitView` 把它们拉得很开、根本测不到重合
→ 补一个远在三亚的活动把视野拉回"看全国"。
e2e 新增 6b 节(**注意必须放在「删除活动」之前**,否则活动连带节点、
参与人一起被删,首页变空态、断言全程空转 —— 首轮就是这样红的)。
本地全链 13 步全绿(渲染检查 370 项、首页浏览器 20 项);远端 e2e 365 项
0 失败;部署静态断言 204 条 0 FAIL、双 PHP 渲染逐字节一致、内容断言全过。
2026-09-24 17:03:55 +08:00
laoyang 064e6b5a5e 路线地图:线允许重合,重合的图标沿线排开(批 15)
批 14 为了治「往返两段标记互相遮挡」,把线段沿法线左右错开画成双向车道。
实测偏移量太大,看着像画错了两条路,用户反馈「两条线偏离的非常多」。

改为**线不动、只动图标**:
- **线段一律按原始坐标画**(去掉 makeSegOffsetter 与法线偏移)。往返行程
  本来就是同一条路,两条线重合是正常的,用户也是这么要求的。
- **重合的图标沿这条线前后排开**:位移方向取线段**在屏幕上的方向**
  (makeSegDir 缓存「不分方向的同一段」的方向,A→B 与 B→A 共用),
  而不是固定往右挪 —— 看上去就是同一条路上跑了两趟,图标仍落在线上。
  线段太短装不下时才退到法线、最后水平兜底。
- 碰撞判定改用**中心距**(Math.hypot < 46,圆标直径 40):斜着挪 56px 时
  两个方盒子在角上仍相交、用矩形判定会说「还重叠」,于是白挪一步
  (图标被拉到 104px 远,看着像没关系)。
- 修一个候选列表的 bug:cand[0](原地不动)也要先判是否可用,跳过它会让
  **第一枚**标记也被无谓挪开一步。

验证:真 AMap 无头浏览器用例扩到 14 项,新增两条同向断言 ——
① 实线顶点必须**逐点等于** data-route 里的原始坐标(一旦又去偏移线段就会偏离);
② 每枚图标到自己那条线的屏幕距离 <= 22px(证明是沿线排开而不是被甩到线外)。
另留一张 map15.png 截图肉眼复核。本地全链 12 步全绿(渲染检查 356 项、
真 AMap 浏览器 14 项);远端 e2e 355 项 0 失败;部署静态断言 201 条、
双 PHP 渲染逐字节一致、内容断言全过。
2026-09-24 16:18:46 +08:00
laoyang 9a11ceff94 路线地图:重合的标记与线段各自让开(批 14)
问题:往返行程(A→B 去程 + B→A 返程)两段的**中点完全相同**、线段也
完全重合,后画的那一枚会把先画的整枚盖住,看起来只剩一段。

两类重合分别处理:
- **线段重合**:同一段(不分方向,A→B 与 B→A 视为同一键)第 2 次起
  沿**法线**错开(长段按比例、短段给下限),像双向车道各占一边。
- **标记重合**:不再只比坐标字符串 —— 坐标不同但屏幕上挨着的两点
  (相差几公里的两个地点,在"看全国"的缩放级别下像素位置几乎一致)
  同样是重合。改为先问地图「这个经纬度现在在屏幕哪个像素上」
  (map.lngLatToContainer),再用贪心排布把撞上的标记按像素左右挪开。
- 到达地红点:同一点位只画一个(往返两端本来就是同一个)。
- **缩放后重排**(zoomend):缩放会改变标记间的屏幕距离,缩小时原本
  分开的标记可能又叠回去。重排时**不再 setFitView**(fromZoom 标志),
  否则与缩放事件互相触发、还会把用户手动缩放好的视野拉回去。

验证:_page11.php 夹具新增返程节点(哈尔滨 → 青岛),_map12.js 新增
「两两求矩形交集」的几何断言 —— 它先抓出真重合(34×39px 重叠),
修复后 0 重叠并确认往返两枚并排 46px、相邻两枚错开 52px。
本地全链 12 步全绿(渲染检查 354 项、真 AMap 浏览器 13 项);
远端 e2e 353 项 0 失败;部署静态断言、双 PHP 渲染、内容断言全过。
2026-09-24 15:48:18 +08:00
laoyang 142b187911 节点表单:参与人直接勾选 + 起点终点记录详细地址(批 13)
参与人(用户反馈:不想等全部填完、保存之后才回详情页加人):
- 节点表单里新增「参与人(直接勾选)」区块:家庭成员按胶囊复选框列出,
  保存即生效;编辑时预勾选已有人。
- Members::syncForNode() 只做增删,**不动留下来的那些人的情绪 / 签到 /
  角色标签**(重新勾一遍不该把之前记的心情清掉)。
- 安全性:新增走 Members::save(),逐条校验「用户属于该活动所在家庭」,
  夹带别家 user_id 进不来;只有请求带 members_present=1(表单确实渲染了
  参与人区块)才同步 —— 否则没这个字段的请求会把已有成员清空。

起点/终点记录详细地址(用户反馈:高德联想只返回简化地名):
- 联想项选中后不再是「青岛北站」就完事:
  ① 有坐标 → 逆地理编码(AMap.Geocoder.getAddress)取该点的
     formattedAddress;
  ② 没坐标 → 按「区县 + 名称」正查,顺带把坐标补回来。
- 详细地址写进 start/end_location 的 address 字段,输入框下方实时显示
  「详细地址:…」,节点详情页本来就展示这一项。
- 路线地图补点(只有地名没坐标时)改为**优先用详细地址**查询,
  比简化地名定位准。

已验证:本地全链 12 步全绿(渲染检查 350 项、真 AMap 浏览器验证 11 项、
表单交互 75 项);远端 e2e 350 项 0 失败(新增 10 项:勾选落库 / 跨家庭
拒绝 / 取消勾选移出 / 情绪不被清 / 无标记不动参与人);部署静态断言、
双 PHP 渲染、内容断言全过后提交。
2026-09-24 12:36:01 +08:00
laoyang c4c3b17cf7 修「光秃秃地图」回归 + 详情页改标签页布局(批 12)
回归修复(用户反馈:地图上什么都不显示):
- 批 11 重构时把 {lng,lat} 对象直接传给了 AMap.Marker.position ——
  AMap 只认 [lng, lat] 数组/LngLat,构造器抛 Invalid Object,
  forEach 当场中断,整张地图一个覆盖物都不剩(光秃秃地图)。
- 现在 position 一律归一化为数组(中点/单点三分支),并新增
  _page11.php + _map12.js 真 AMap 无头浏览器用例锁死(覆盖双坐标/
  单坐标/只有地名三种节点形态 + 无 JS 报错 + 标记数量断言),
  已接入 _chain3.py 全链(步骤 10b)。

标签页布局(用户需求):
- 活动详情页拆成两个标签:「🗺️ 行程地图」(默认)与「📋 计划与实际」;
- 地图页独占整页,高度加大到 clamp(480px, 72vh, 860px)(手机 62vh);
- 节点页 = 原 时间轴(计划/实际两列)+ 评论区,默认隐藏;
- 切回地图标签时重新 setFitView(隐藏期间容器尺寸无意义);
- 活动没有可上图的执行节点时不渲染标签条,退化为原单页布局。

已验证:本地全链 12 步全绿(渲染检查 341 项 + 真 AMap 浏览器验证
11 项);远端 e2e 341 项 0 失败;部署静态断言/双 PHP 渲染/内容断言
全过后提交。
2026-09-24 12:04:31 +08:00
laoyang 00cd99d07d 路线地图体验修正 + 节点表单裁剪改按钮触发(批 11)
地图四项调整(用户反馈):
- 位置移到封面之下、时间轴之前 —— 行程一长不用拉到底才看到地图;
- 圆形标记放在该段行程线的中点(起终点都有才取中点,缺一头放
  有的一头),不再是起点位置;anchor 改 center 居中骑在线上;
- 只有地名、没坐标的地点(偏僻地方下拉选不到点位):浏览器端调
  AMap.Geocoder 按地名查坐标,查到补进路线全量重画(drawRoute 幂等),
  查不到保持缺省不硬造位置;data-route 新增 sname/ename 地名;
- 提示文案同步更新。

裁剪交互(用户反馈):
- 选图后不再自动弹出裁剪框;每张能裁的缩略图旁放「✂ 裁剪」按钮
  (左上角,与右上角 × 对称),点了才弹框,只处理这一张;
- 「逐张裁剪」批量入口保留:确定后仍自动弹下一张(cropChain 标志
  区分两种流程);删除 addedCroppable 自动触发逻辑。

已验证:本地全链 11 步全绿(渲染检查 339 项、真实浏览器交互更新为
按钮触发流程 75 项);远端 e2e 340 项 0 失败(新增 9 项断言);
部署静态断言/双 PHP 渲染/内容断言全过后提交。
2026-09-24 11:41:11 +08:00
laoyang 69279db1b2 活动详情页新增「行程路线」地图(高德,批 10)
按时间顺序把执行节点(reality)的起终点坐标连成路线:节点内实线
(showDir 方向箭头)、节点之间虚线衔接;每个节点在出发地(没有出发地
就放到达地)放一个圆形标记 —— 有图片附件显示图片,没有就显示节点
类型 emoji,左上角带序号;点击标记/红点打开对应节点详情。到达地与
出发地不同点时另画红色小圆点。

实现要点:
- 数据层 Attachments::firstImageIdsForNodes():一条 GROUP BY + MIN(id)
  批量取各执行节点首张图片附件,防 N+1;
- 模板只收「有坐标的执行节点」,计划节点与无坐标节点不上图;
  路线数据经 data-route 属性以 JSON 交给内联 JS;
- AMap 2.0(layout.php 已全局加载),加载失败时给一句人话兜底不白屏;
- 地图区块位于时间轴之下、评论区之前。

已验证:本地全链 11 步全绿(渲染检查 330 项);远端 e2e 全绿(新增
10 项地图断言);部署静态断言与双 PHP 真实渲染通过后提交。
2026-09-24 11:12:27 +08:00
laoyang c50bc7de2b 活动页:每个节点卡片底部渲染参与人(头像 + 名字)
之前节点卡片上只有「👥 N」计数,要看是谁参加得点进节点详情。
现在卡片底部直接列出参与人:头像(缺头像用首字母兜底)+ 名字,
有心情顺带一个小 emoji;点任一人跳到该节点的参与区。
数据侧 Members::forNodes() 一条 IN 查询带回全部节点的参与人
(与 attachCounts 同样的防 N+1 思路),计划列与执行列共用。
无参与人的节点不渲染这一行。

已验证:本地全链 11 步全绿(渲染检查 292 项、表单交互 73 项);
远端 e2e 299 项 0 失败(新增活动页卡片参与人行专项断言);
部署后双 PHP 渲染逐字节一致、60 文件哈希一致。
2026-09-23 15:18:25 +08:00
laoyang 48237c8947 节点详情:照片灯箱浏览、参与人点击编辑、花销只记总计
1) 照片与附件不再新开窗口看
   之前点缩略图直接 target=_blank 打开原始文件,看完还得切回来。
   现在统一走当前页灯箱(templates/_lightbox.php,详情页照片墙与
   照片管理页共用):左右箭头 / 键盘 ←→ / 手机滑动切上一张下一张,
   Esc 或点背景关闭,带「第几张 / 共几张」计数并预加载相邻两张。
   非图片附件(PDF 等)仍保留直接打开与下载。

2) 参与人可以直接改了
   之前加完只能移出重加。现在每个参与人旁有「编辑」,点开预填弹窗
   (角色标签 / 心情 / 签到时间),保存仍走 members/store ——
   Members::save() 本来就是 upsert(同节点同人一条),服务端零改动。
   失败回填按 user_id 精确匹配,只回填刚才没保存成的那个人。

3) 花销去掉预算,只记真实花费
   明细上移,底部一行「总计(N 笔)¥X」。表单不再有「预算」选项,
   查询一律 is_budget=0(历史预算行不再出现在列表与合计里);
   活动页统计同步改成「共 N 笔 · 总计」。列保留在库里不删数据。

已验证:本地全链 11 步全绿(php -l / 渲染 286 项 / SQL / PDO / CSS /
Mime / amap / 内联 JS×11 / 布局几何 / 表单交互 73 项);远端 e2e
296 项 0 失败(含灯箱、编辑弹窗、历史预算行排除的专项断言);
部署后双 PHP 渲染逐字节一致、60 文件哈希一致,线上 style.css 含新规则。
2026-09-23 14:59:02 +08:00
laoyang f9a624ef4b 交通字段简化 + 照片多图/裁剪/双存储后端 + 界面重构
交通字段
- 飞机只记航班号、火车只记车次,不再收座位号 / 线路 / 承运人 / 登机口
  (多人同乘一趟时座位号本来就无意义)
- trip_nodes 的历史列保留但不再写入也不再渲染,编辑一次即被显式清成 NULL
- 节点表单按出行方式切换号码标签(航班号 / 车次 / 班次 / 线路 / 航次)

照片
- 一个节点可传多张照片,前端支持逐张裁剪(Cropper.js,只把裁过的走 base64)
- MIME 由自建的 src/Mime.php 按内容识别:getimagesize → 魔数嗅探 → 扩展名映射,
  刻意不依赖 fileinfo —— 线上同时装着 PHP 7.4(没装该扩展)与 8.1
- 单张体积上限、可传张数在设置页可调

双存储后端
- 本站 uploads/ 或阿里云 OSS,在「存储设置」页配 endpoint / 桶名 / 前缀 / 内网开关;
  OSS 配置不完整时静默回退本地,避免管理员填错就把上传整个弄挂
- OSS 鉴权支持 ECS 实例 RAM 角色换 STS 临时凭证(临时凭证缓存在 /tmp,不落 Web 目录),
  也支持直接填 AccessKey
- OSS V1 签名手写实现,不引 SDK(本项目无 composer / 无 autoload)

媒体代理
- 照片与活动封面一律经 media/show、media/cover 代理输出,先校验家庭归属再吐字节
- 页面里不出现 OSS 域名,也不出现本地真实路径;桶保持私有
- 响应带 nosniff,并关闭 display_errors,免得 PHP 报错混进图片正文把图弄坏

数据库
- 新增迁移机制(src/Migrate.php + tools/migrate.php,仅 CLI 可跑)
- 建 app_settings 表;attachments 增加 storage / object_key / mime_type / size_bytes /
  width / height / is_image 与 idx_storage
- MySQL 8 不支持「加列时带 IF NOT EXISTS」,迁移一律先查 information_schema

界面
- 顶栏只留品牌与「新建活动」,其余入口收进右上角下拉菜单(details/summary,零 JS 可开合)
- 新增「存储设置」页:当前生效存储、数据库结构版本、是否 ECS 运行环境三张状态卡,
  外加测试连接与角色探测
- 样式表整体重写

验证
- 模板渲染检查 234 项、SQL 列名静态检查、模板到 CSS 的类覆盖检查、内联 JS 语法检查全通过
- 已部署到 trip.yangyq.net 并完成线上验证:双 PHP(8.1 / 7.4)14 条路由渲染字节数完全一致、
  26 项内容断言全部通过、照片代理取件与磁盘原件逐字节一致(69/69 字节)
2026-09-22 23:09:37 +08:00
laoyang a79956758b 补齐未接入代码的功能域:参与人 / 评论 / 花销与分摊
数据库里早就建好了 comments、node_members + role_tags、expenses +
expense_splits 三组表,但一直没有代码接入。本次把这三块连上业务,
并新增「节点详情页」作为聚合入口。

新增
- src/Access.php          统一归属校验。此前每个控制器各写各的 family_id
                          条件,容易漏;现在 activity()/node()/target() 一处收敛,
                          requireAdmin() 统一管理员判断。
- src/Comments.php        两级平铺:parent_id 只允许指向顶层,写「回复的回复」
                          时规范化回顶层;删顶层会连同回复一起删。
- src/Members.php         参与人 + 角色标签 + 情绪 + 签到。情绪目录 17 项,
                          全部选用 Emoji 11.0(2018)及以前的码位,避免旧系统
                          字体缺字形显示成空白方块。
- src/Expenses.php        花销与分摊。分摊按「分」为单位 intdiv 平均,
                          余数归最后一人,保证 SUM(share_amount) = total_amount。
- templates/nodes/show.php 节点详情页:基本信息 + 参与人 + 花销 + 讨论 + 附件入口。
- 角色标签管理、评论区、参与人区、花销区各自的模板与控制器。

修复
- 越权:往别人活动的节点上插节点、把别家用户加成参与人,都只校验了
  「节点存在」,没校验「用户与节点同家庭」。现统一走 Access。
- 删活动时节点附件文件删不掉:附件在 /uploads/attachments/ 下,却调了
  只认 /uploads/covers/ 的删除函数,判定不通过 → 文件永久残留。改为
  统一的 deleteUploadFile(),两个目录都认。
- 删有引用记录的用户会 500(activities.created_by / attachments.uploader_id /
  comments.author_id / expenses.payer_user_id 这几处没有级联,也没做前置检查)。

其他
- 活动/节点列表补上附件、评论、参与人、花销四个计数(一次 GROUP BY 查完)。
- 首页布局新增一条迁移易踩的规避:新列上的索引必须排在 ALTER TABLE 之后。
- 新增 .gitignore:config.php(含数据库口令)与 uploads/ 下用户上传的
  头像封面不入库,只保留 uploads/.htaccess 那条禁止执行脚本的规则。
- 新增 config.example.php 作为新环境的配置模板。

本批不改数据库结构,不需要任何数据迁移。

本地与线上验证
- php -l 全绿(PHP 8.4 本地 / 线上 7.4.30 与 8.1.7 双版本)。
- 模板渲染检查 180 项全通过。
- 线上端到端 153 项全通过:登录、活动增删改查、节点流转、
  参与人/角色标签/情绪/签到、评论、花销与分摊、权限、级联清理、
  全站页面无 PHP 报错泄漏、跑完基线逐表核对且既有 id 集合逐字未变。
- 顺带写了 trip_sql_column_check.py:拿真实库表结构静态校验全项目 SQL 的
  列名引用。本次就是靠它之外的 e2e 抓到 travel_modes 并没有 line_name
  (该列属于 trip_nodes),一个列名写错导致节点详情页整页 Fatal。
2026-09-22 22:03:08 +08:00