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
|
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
|
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 |
|