laoyang
|
02f40b6d6f
|
路线地图/节点卡片/详情按出行方式显示不同图标(批 19)
出行方式(飞机/火车/游船/自驾/地铁…)原本在路线地图标记、节点卡片交通行、
节点详情「出行方式」行一律显示节点类型图标(赶路节点恒为 🚗),易让人误解。
- TravelModeController 新增 MODE_ICONS(mode_type → emoji 映射)
- ActivityController::show() 两路查询 LEFT JOIN travel_modes 取 mode_type,注入 modeIcons
- 路线地图标记 / 卡片交通行 / 详情出行方式行:优先用出行方式图标,
未选出行方式时回退节点类型图标
|
2026-09-24 20:08:17 +08:00 |
|
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
|
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
|
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
|
cbfdcb9d16
|
附件:支持视频 / 文档上传,可预览的浏览器直接看,预览不了的给下载
之前节点附件只能传图片和 PDF(accept 过滤 + 20MB 上限),视频传不上来。
本批放开:MP4 / MOV / PDF / Word / Excel / ZIP 都能作为附件上传,
仍走同一份 Uploads::handleFiles(MIME 白名单三层识别不变),存进已配置的 OSS。
大小分级:照片仍走 photo_max_mb(管理员可调),视频与文档放宽到 40MB
(Uploads::MAX_FILE_BYTES,给线上 post_max_size=50M 留余量),先 detect 再按
is_image 选上限。前端 form.php / attachments.php 同步拦截并按类型细分暂存区图标。
预览走灯箱四类型视图:图片(img)/ 视频(<video>,配合 media/show 新增的
HTTP Range 单区间 206/416,进度条可拖、Safari 也能播)/ PDF(iframe 内嵌
阅读器)/ 其它(Word / ZIP 等浏览器预览不了 → 灯箱里给下载按钮)。
dl=1 下载不接 Range;多区间回退 200 全量。
已验证:本地全链 11 步全绿(渲染检查 308 项、表单交互 73 项);
远端 e2e 0 失败(新增 mp4 上传落库 video/mp4、Range 206/416/后缀区间/
多区间回退/dl 不接 Range 专项断言);部署后双 PHP 渲染逐字节一致。
|
2026-09-23 16:22:01 +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
|
86e0481a19
|
节点表单:24 小时制时间控件、附件可裁剪、保存失败不丢已填内容
1) 时间选择不再出现「上午/下午」
datetime-local 的显示格式由浏览器 UI 语言决定(中文环境渲染成
「下午 03:30」),页面与元素的 lang 都压不住(截图实测)。
拆成「原生 date 控件 + 时下拉(00-23) + 分下拉(00-59)」组合控件
(templates/_datetime.php),24 小时制由控件本身决定,浏览器改不掉。
服务端 read_dt_input() 把三段拼回 Y-m-d H:i:s 并校验,兼容旧单字段。
节点起止时间与成员签到时间一并换掉。
2) 附件可裁剪,不想裁点「确定」就按原图上传
裁剪弹窗抽成 templates/_crop_modal.php 与照片页共用;表单里选完图
自动逐张进入裁剪,整图选框=没裁(保留原文件、不重新编码、省 base64
流量),裁过的走 cropped 通道(隐藏域 JSON,原生表单提交也能带),
没裁的留在 input.files,已裁卡片带角标,可随时重置重来。
3) 节点保存失败不再清空整页
服务端:失败分支 flash_old($_POST),重开的表单按 old()/old_array()/
old_dt() 全字段回填(含勾选删除状态),渲染完即清(layout.php)。
前端:sessionStorage 草稿兜底服务端拿不到 $_POST 的情况
(post_max_size 超限、会话过期),恢复条件收窄到「页面有错误提示条」,
避免陈旧草稿污染新表单。
已验证:本地 php -l 全量 0 错、模板渲染 275 项、表单交互(真实浏览器)
73 项——含真实裁剪一刀、草稿恢复、三段式时间选择;远端 e2e 289 项
0 失败(存储设置测试期间归一为 local,不再写用户真实 OSS 桶,收尾还原);
部署后双 PHP 渲染逐字节一致、59 文件哈希校验一致。
|
2026-09-23 09:11:42 +08:00 |
|
laoyang
|
44ffee6c92
|
节点表单:时间框整块可点、起点终点一键导航、可直接传多张附件
三件事都是「界面上明明该有、但用起来不通」的地方:
1) 时间输入框只能点右侧那个小日历图标才弹选择器
原生行为如此(而且各浏览器还不一致),用户会以为输入框是只读的。
在全局脚本里给 datetime-local / date 挂 click → showPicker(),
等于把整块输入区都当成那个图标;选择器已在显示时再调是空操作,
不支持的浏览器静默跳过。顺带给可点光标。
写在 layout.php 里,所以签到时间等其它时间框一并生效。
2) 起点/终点的精确位置此前只存在页面上,没有落库
高德选点回调里拿到的 lng/lat 现在随节点一起存库,节点详情页和
表单页都生成高德导航链接,点一下就能直接导航过去(手机端会尝试
唤起高德 App)。链接只给 to 不给 from,让移动端自动用当前位置当起点。
⚠️ 高德 URI API 的坐标顺序是「经度,纬度」,写反了不报错、只会安静地
导到地球另一边,所以单独写了单元测试守住顺序。
手工改地名时会清掉旧坐标,退化成关键字搜索,避免「名字改了位置没改」。
3) 节点处不能上传图片附件
节点表单改成 multipart,加拖放区 + 多选 + 本地预览 + 逐个移除,
编辑态可勾选删除已有附件(逐条校验归属,防止改 id 删到别人家的文件)。
上传逻辑从 AttachmentController 抽到 src/Uploads.php,两处共用一份:
MIME 白名单、大小上限、孤儿回滚各抄一份迟早走偏。
请求体超过 post_max_size 时 PHP 会静默丢掉 $_POST 与 $_FILES,紧随的
CSRF 校验必然失败、报「请求已过期」——这是最难排查的一类问题。
现在按 CONTENT_LENGTH 提前识别并给出人话提示,且该守卫必须排在
csrf_check() 之前(只读长度、不改状态,所以顺序上是安全的)。
已验证:本地 php -l 全量、模板渲染 255 项、amap_nav_url 单测 18 项、
真实浏览器交互 47 项(时间框三处点击都弹、多选/移除后 input.files 真被
改写、超限被前端拦下、导航链接随选点刷新、勾选删除变红、三视口无溢出)、
打包 57 文件、远端 e2e 267 项,全部通过;部署后双 PHP 渲染逐字节一致。
|
2026-09-23 07:53:20 +08:00 |
|
laoyang
|
43171597e2
|
给静态资源加版本号,修掉「服务端更新了但界面没变」
现象:CSS 更新并上线后,用户打开站点看不到任何变化,怀疑没部署。
实测服务器上的 style.css 已是新版(与工作树逐字节一致),真实浏览器
直连线上也拿得到新文件,所以问题在客户端缓存:
· 页面 HTML 带 no-store,每次都重新渲染 → HTML 一直是新的;
· 但 /assets/*.css|js 的响应没有 Cache-Control / Expires,浏览器只好
退回自己的「启发式缓存」——在它自行估算的新鲜期内根本不发请求;
· 而 <link href="/assets/style.css"> 的地址从不改变,文件换了浏览器
也无从得知。于是出现「HTML 新的、CSS 旧的」,界面看起来毫无变化。
修法:新增 asset() 辅助函数,拼上 mtime + 文件大小做 cache busting,
文件名不变而内容一变、地址就变,浏览器只能重新拉取,普通刷新即生效。
版本号同时带上文件大小,挡住「mtime 没变但内容变了」这种漏网情况;
取不到文件信息时不加版本号,退化成原行为,不影响可用性。
已验证:线上 HTML 现在引用 /assets/style.css?v=…;真实浏览器直连线上
三个视口(1920/1280/390)均无横向溢出,资源全部 200;1920px 下顶栏品牌
与正文左边缘同为 420px,头像 26×26(源图 400×400)。
|
2026-09-23 07:06:36 +08:00 |
|
laoyang
|
96dceb1a51
|
修复上线后暴露的两处回归:节点写路径 HY093、类型白名单可被扩展名绕过
两处都是端到端测试抓出来的,php -l 与模板渲染检查都碰不到。
一、新建 / 编辑执行节点整条失效(SQLSTATE[HY093])
上一提交为「不再写历史交通列」,把 line_name / carrier / seat_info / gate
四个占位符从 NodeController::store() 与 update() 的 SQL 里删掉了,但
collectInput() 返回的 $data 仍然带着这四个键(值恒为 null)。
本项目 Database 用的是原生预处理(ATTR_EMULATE_PREPARES => false),
参数数组里多一个 SQL 没有的键会直接抛 HY093 —— 于是:
nodes/store 失败 → nodes/edit 取不到 CSRF → 依赖它的用例连锁失败,
e2e 一度 106 通过 / 125 失败。
修法:两处 SQL 恢复这四个占位符,值显式写 null。
这既让参数个数对上,也顺带在每次编辑时清掉历史脏数据。
已在代码里写明「删不得」的原因,并在部署脚本里加一条静态断言
(数 ':line_name' 出现次数,期望 2)当哨兵。
另新增 _tooling/trip_pdo_param_check.py:静态比对 prepare() 的命名占位符
与 execute() 数组的键。对删除前版本精确报 2 处,对修复后 0 处。
二、改个扩展名就能绕过上传类型白名单
Mime::detectFile() 的第三层「扩展名兜底」让「内容认不出来」的文件凭文件名
进入白名单 —— 一个内容是纯文本、名字叫 .jpg 的文件被判成 image/jpeg。
改成 refineByMagic():扩展名只能用于**细化**已经认出来的容器类型,
把 application/zip 细分成 OOXML(docx / xlsx)、把 OLE2 细分成 doc / xls,
不能把 null 变成任何具体类型。内容认不出来就是认不出来。
配套新增 _tooling/trip_mime_check.php 单元测试(27 项,不连库不起服务),
重点守住「改后缀不能绕过白名单」这条安全语义。
验证
本地:php -l 55 文件 0 失败;模板渲染 234 项全通过;SQL 列名 15 张表无未知列;
PDO 占位符 0 问题;CSS 202 个类全有定义;Mime 单元测试 27 项全通过。
线上:部署脚本 10 步 EXIT=0,双 PHP 渲染 14 条路由字节数完全一致,
26 项内容断言全通过(含照片代理 69/69 字节逐字节一致);
端到端测试 239 项通过 / 0 项失败(修复前 236/3)。
|
2026-09-22 23:30:59 +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 |
|