面向开发的变更记录(技术细节)。群友版更新请看 更新公告。
[Unreleased]
[2.4.7] - 2026-09-15
Fixed
restsOk 降级为"未知");② _dup_override 剥离回写条件修正为"键原存在即回写"(原 delete 后判断恒 false 致永不回写、非 true 形态键会透传致未知列 400 自伤;单测抓出并补 _dup_override:false 用例);③ 上传 60s timer 改 try/finally 清理(abort/断网抛错路径不再残留 timer,两流程一致);④ 客户端查重与服务端语义对齐:同名不同区=分店不弹重复警告(blur/submit 传区域);⑤ 放大角标加 aria-hidden(读屏不重复播报)。另修自查抓到的既有 bug:submit 查重命中时 dupMatchName 误赋对象致显示 [object Object](改 .name)。单测 90 项全过。真机冒烟(软删/建店/写评价)通过。Security
UPDATE(deleted)、edit_log 仅 SELECT、contributions/likes 零权限),与 RLS 软删策略(qual deleted=false/with_check deleted=true)+列级 GRANT 三层一致;② F9 闭环——6 个 admin_* SECURITY DEFINER 函数 anon 均不可执行,代码零调用保留不删;③ CF Rate Limiting 定案 A 不建(WAF 限流为 zone 级、pages.dev 无 zone;威胁模型匹配现有应用内限流);④ ADMIN_PASSWORD 强度残余风险经用户评估接受(standing offer:登录失败锁定);⑤ 新缺口入档:Supabase 免费档无自动备份,例行导出机制待建。[2.4.6] - 2026-09-15
Changed
pointer-events-none 不挡点击),标题行只留「群友实拍 (N 张)」。[2.4.5] - 2026-09-15
Fixed
_dup_override:true,代理见标记跳过去重并**转发前剥离该键**(防未知列插入);② 新增分店语义:**同名但区域不同直接放行**(select 增 district 比对),同店名不同区域不再误拦;③ 模糊包含匹配仍须确认(防手滑重复)。新增 tests/supabase-dedup.test.mjs 4 用例(同名同区 409 / override 放行且剥离键 / 同名不同区放行 / 模糊仍 409);顺带修路由 import proxy-guard 缺 .ts 扩展名致 Node 类型剥离模式无法解析。FileReader.readAsDataURL——对数 MB 相册图在**主线程同步 base64 编码**并生成数 MB 字符串,移动端低内存设备选图即冻住(与 CSP 无关,自查已排除:生产 /api/upload 带同源 Origin 返回 200、CSP img-src 已含 blob:/data:)。修:预览改 URL.createObjectURL(blob: 零拷贝引用),换图/移除/提交成功时 revokeObjectURL 防泄漏。顺带修两个自查发现的真缺陷:① 新建页上传失败原被**静默吞掉**(console.warn 后继续提交→图片丢失却提示成功),改为报错并中止提交;② 上传无进度无超时,慢网络表现为无限"提交中"——加按钮阶段文案「上传中...」+ 60s AbortController 超时(超时提示换小图/查网络),两流程(新建/评价)一致。delete-restaurant 硬删成功后连带物理删除该店 contributions(contributions 建表无外键,须代码级联;评价由 DB 外键 ON DELETE CASCADE 连带);② **软删隐藏**:盘点列表不再列入软删店的追加(店还原后追加自然回来,语义与"删除=隐藏"一致);③ **孤儿兜底**:餐厅行已不存在的孤儿行标 orphan、后台红底标识待清理残留,店名从硬删审计快照(edit_log 的 hard_delete_restaurant 行 old_value)捞回**按真店名显示**(用户要求不叫"孤儿店"),支持手动批量删除(硬删连带清理万一失败的兜底)。单测 +2(生命周期过滤/孤儿标记/店名回收、硬删级联)。Security
default-src 'self'; script-src/style-src 'self' 'unsafe-inline'(内联脚本/样式存量起步,nonce 改造另列专项);connect-src 'self'(站点无外部运行时资源,生产 Supabase 全走同源 /api/supabase 代理);img-src 'self' blob: data:(上传预览 object URL);object-src 'none'、base-uri 'self'、form-action 'self'、frame-ancestors 'none'。双处生效:SSR middleware(仅 PROD 构建——astro dev 前端直连 Supabase 属跨域,加 CSP 会误伤开发链路)+ 静态 _headers(预渲染路由)。离线验证:SSR/预渲染两类路由 CSP 头均在、四类页面 200;**待真机冒烟**(首页/详情/后台/添加/图片上传预览/编辑弹窗全功能过一遍)。Changed
constants.ts 公共模块抽取:分类/标签/标准区域/推荐菜拆分正则(CATEGORIES/TAG_OPTIONS/STANDARD_DISTRICTS/DISH_SEP_RE/splitDishes)收敛到 src/lib/constants.ts 唯一事实源;edit-restaurant(再导出保持 import 路径兼容)、EditRestaurantModal、add.astro、server-data、contributions 的私留副本全部移除。顺带删除 validateField 中 recommended_dishes/notes 死分支("只增不改"后二者已不在 EditableFieldName,比较恒不成立,校验由 validateContribution 承担)。添加表单区域选项顺序统一为 constants 序(标准区 + 全区域)。astro check 类型门禁:历史 260 个类型错误归零——逻辑层 3 个真错误实修(ts2367 死分支、ts2345 Set 泛型与 slug undefined 兜底);三页 6 个内联 DOM 胶水脚本块(add×1、admin×1、[slug]×4)以 @ts-nocheck **显式登记类型债**(债清单见 HANDOVER,待脚本模块化后统一移除,非静默忽略);deploy.yml 新增 Type check 步骤(Test 后、Build 前)。新增 devDeps:@astrojs/check、typescript。[2.4.4] - 2026-09-12
Security
/img Content-Type 白名单收敛——不再优先信任桶内 httpMetadata.contentType(历史迁移对象若含 SVG/HTML 即同源存储型 XSS),仅 4 种图片类型可信(declared 去参数优先、扩展名兜底),其余降级 application/octet-stream + Content-Disposition: attachment;离线五形态探针全符合。**F6** 代理转发前无条件剥离客户端自带 apikey/Authorization/Accept-Profile/Content-Profile 再重注入 anon key(原"仅缺失时注入"透传伪造鉴权头/schema 头);真链路验证恶意头被替换/剥离。**F8** 旧管理密码明文从 HANDOVER 删除(口令早已轮换,git 历史字面量随之失效)。**F11** HSTS max-age=31536000; includeSubDomains 双处生效(静态 _headers + SSR middleware;不加 preload,注册不可逆)。**CSP 缓办**:大量内联脚本需 unsafe-inline 起步或 nonce 改造,单独一轮评估+真机冒烟后再上。c.id !== excludeId,bigint 字符串 vs Number 恒真)致本人仅大小写/空格差异的自改被自己旧值误判重复 → String 化比较 + 回归单测;② **中**·硬删(餐厅/评价/贡献)无审计,批量硬删不可追查 → 加快照审计(删前 fetchSnap 取快照、删后写 edit_log approved,best-effort 不阻断已执行的物理删除);③ **低**·追加可向不存在餐厅 id 写孤儿行 → 追加前校验存在(软删隐藏店仍放行,保生产自测约定);④ **低**·GET /api/contribution 已无调用方且 deviceToken 走查询串有日志泄露面 → 端点下线、限流简化为仅写;⑤ **低**·sf_device_token cookie 缺 Secure(现承担 SSR 归属判定)→ 补;⑥ **低**·admin/auth/edit-restaurant 同源端点多余 ACAO:* → 移除(F7 收口)。**留**:审计写失败但主写成功的半提交提示文案(上轮已改清晰,可接受)。白名单核对:本会话无新增未登记浏览器端 Supabase 调用;XSS/鉴权/注入面复核闭环。Added
contributions 表——后台不可见、无审计、无提醒,发现全靠人工撞见。本次:① 追加成功后 addContribution 写一行 edit_log(status=approved 表示自动生效,不进「待处理」列表,不改变产品行为);② 新增管理端盘点端点 /api/admin/list-contributions(POST 读、token 鉴权、service_role join 店名;contributions 不在代理读白名单内,盘点必须走服务端);③ 后台新增「最近追加(近 20 条)」模块:店名/字段/内容/署名/时间列表,疑似测试内容(temp/test/纯数字/超长)标黄,每行一键删除(复用 /api/admin/delete-contribution)。新增端点单测与追加端点级单测;离线 e2e:未登录盘点 401、登录盘点返回追加+店名、追加留 approved 痕。Fixed
Prefer: undefined → #id → 加载失败):list-contributions 店名获取历经三次真机事故:① 两请求 join 时 postgrestHeaders 缺省 prefer 写入 'Prefer: undefined' 头(已修:有值才写);② 真机店名全显示 #id(join 失败);③ 改嵌入式查询(select=*,restaurant:restaurants(id,name))后,因**生产库 contributions 无外键**嵌入 400 → 整模块"加载失败"。终态为**三级容错**:嵌入单请求(需 FK)→ 回退顺序两请求(先 contributions 再 id=in.(...) 取店名,String 键 join 防 bigint 字符串错位)→ 店名取不到回退 #id,列表永远可加载。单测覆盖嵌入成功与嵌入 400 回退两路径。另提供可选外键 DDL sql/add_contributions_fk.sql(含孤儿预检语句、ON DELETE CASCADE 顺带清孤儿追加;执行后嵌入路径生效,非必需)。applyAppend 移除:CHANGELOG 2.4.3 所述"edit_log 审计"实际指向死代码 applyAppend(自 acdbee8 改走 contributions 后生产链路无人调用),其单测造成"追加有审计"的假象,掩盖了生产链路完全无留痕的缺口。本次审计改为在 addContribution 内真实写入;applyAppend 及其专属符号(APPENDABLE_FIELDS/AppendableFieldName/normalizeEntry)移除,墓碑注释指向真实链路;其单测改端点级(tests/append-restaurant.test.mjs,直接断言生产链路 /api/append-restaurant → contributions + edit_log)。[2.4.3] - 2026-09-11
Fixed
GET /api/contribution 后重建 DOM,真机该拉取偶发失败即退化为服务端兜底的全灰只读 chip,本人条目恒不可改/删(服务端 curl 带同 token 返回 mine:true 正常,根因在客户端拉取环节)。现改为 **SSR 读同源 cookie sf_device_token → LIKE_SALT 加盐哈希 → 与 contributions.device_hash 比对**,弹窗在服务端即区分本人/他人;客户端不拉取归属,彻底移除 loadContribs / window.__loadContribs。归属判定失败(无 cookie / 无盐)时全部按他人只读渲染,不影响详情页主体。真机验收同期两处修正:①**"点不了"**——改/删绑定脚本位于弹窗 DOM 之前,按容器绑定时容器尚不存在致按钮无事件,改 **document 级事件委托**(与 DOM 顺序无关);②**视觉**——按反馈弃橙色填充 chip(突兀),补充内容改**列表行**(内容 + 署名),本人行右侧低调灰色下划线「改/删」、他人行无按钮。改/删成功后整页 reload 由 SSR 重渲染。1a850bb、d63bcbb):真机发现打开编辑弹窗时补充条目偶发不显示(客户端拉取归属失败即空白)。改为**服务端先把贡献渲染成灰 chip**(客户端拉取成功后再升级为"本人橙色+改/删",失败也保留灰 chip);并将 /api/contribution 读/写限流分桶(读 30/分、写 10/分),降低打开弹窗的读被写流量误限 429 的概率。Added
acdbee8):响应群友"手滑打错不能全靠管理员"。补充的推荐菜/备注不再并入基线文本,而是存入 contributions 表(sql/add_contributions.sql:同店同字段内容唯一 + RLS deny-by-default)并记录追加设备的加盐哈希。**已上线并验证**:追加、去重、详情/首页合并展示、弹窗服务端灰 chip 可见(1a850bb)、读/写限流分桶(d63bcbb);生产 e2e(隐藏店 388,curl 层)追加→重复拒→本人改→他人改拒→他人删拒→本人删→列表空 全符合。**本人改/删自己那条**:初版依赖客户端拉取归属升级 chip,真机不可用;已改 SSR 判定归属修复(见本节 Fixed「本人补充条目改 SSR 判定归属」),待真机验收。EDITABLE_FIELDS 移除这两项(服务端强制不可整段覆盖);新增 applyAppend(追加 + 归一化去重 + 一次一道菜/一行备注 + 长度上限)与 /api/append-restaurant(每 IP 限流 + service_role + edit_log 审计)。编辑弹窗中两字段改为「现有内容展示(推荐菜 chips / 备注行)+ 补充输入」,不再整段 textarea。管理后台新增「整段编辑推荐菜/备注」(搜索→载入→保存,/api/admin/edit-fields,token 鉴权 + 审计 approved),保证管理员仍具整段整理/纠错能力。新增 tests/append-restaurant.test.mjs(5 用例),全套 78 单测过。**生产 e2e(对已软删隐藏测试店):追加生效、重复追加被拒、备注行追加、edit_log 留痕;群友覆盖式提交被服务端拒绝("没有要修改的字段")。[2.4.2] - 2026-09-11
Changed
333840e):星星 / 分数 / ⓘ /(N 人评分)改 flex items-center 同一排垂直居中 + leading-none 收紧(原 inline 拼接致 ⓘ 基线下沉错位)。[2.4.1] - 2026-09-11
Changed
6cee85e→1064dbb):气泡内容改**单行**(whitespace-nowrap+w-auto,去掉固定宽度导致的折行);共**三处** ⓘ(吃过 / 评价区平均分 / 写评价表单"点击评分"旁)且**统一朝右弹**(left-full 垂直居中、左箭头);评价区平均分 ⓘ 置于分数紧后、表单 ⓘ 紧跟"点击评分"——触发器贴屏幕右缘时朝右弹会溢出手机屏,故调整位置;平均分 ⓘ 的注入在评价区客户端渲染的平均分标记内(生产 restaurants.rating 列全空、信息卡分标签不渲染,故挂评价区才是群友真能看到的位置)。[2.4.0] - 2026-09-11
Added
335bfb4):无账号"一设备一票"点赞。新增 likes 表(sql/add_likes.sql:unique(restaurant_id, device_hash) + RLS deny-by-default + 索引)与 /api/like 端点(服务端 LIKE_SALT 加盐哈希设备 token、service_role 写、点赞 Prefer: resolution=ignore-duplicates 幂等、每 IP 限流、缺盐 fail-closed 500);详情页点赞按钮(可撤销)+ 计数 + 注明("赞过=认可或去过人数;打分看评价星级"),首页卡片 meta 行加「👍N 赞过」(SSR service_role group-by 实时计数,不存易漂移列)。点赞**纯展示,不进任何排序/筛选/榜单**(用户明确拒绝排行榜)。重复提交(409/名称预检)新增「去点赞这家 / 去写评价」出口,把被拦下的重复推荐转成对已有店的支持。新增 tests/like.test.mjs(7 用例)。**生产端到端已验**:基线 200、点赞 count+1、同设备重复点不重复计数(唯一约束生效)、取消赞回基线、首页角标渲染、测试数据净零。Security
e34e6c3):B3 白名单此前只校验 PATCH body 为 {deleted:true},未校验过滤条件——构造 PATCH /rest/v1/reviews?id=gt.0 + {deleted:true} 一条请求即可经代理软删全站评价/餐厅。现要求 PATCH 的 query 恰为单主键等值过滤(id=eq.<整数>,或删店级联软删评价用的 restaurant_id=eq.<整数>),其余(gt/gte/无过滤/多条件/or=(...)/非整数/非主键列)一律 403。核实前端三处 update 调用的过滤列后才收紧,不误伤正常删除;新增 F5 拦截用例,全套 66 单测通过。线上回归:合法过滤 204(0 行受影响)、批量过滤 403、未删数据完好、删除功能正常。Changed
src/lib/ai-note.ts(parseAiNote/mergeAiNote + AiNote 类型),消除此前 4 份重复实现——edit-restaurant.parseAiNote(改为再导出,对外 import 路径不变)、server-data.parseNote(变体,返回 {}/undefined 且 p 未校验有限性)、[slug].parseDetailNote(与规范版逐字节相同)、index.astro 内联 delivery 解析。规范行为:解析失败/缺字段一律回落具体默认值 {tags:[],avgPrice:null,delivery:false};p 仅接受有限数字(NaN/Infinity→null,比旧 server-data 变体更严);d 仅接受布尔。新增 tests/ai-note.test.mjs(10 用例),全套 65 单测通过。**#10 后半(constants.ts:CATEGORIES/区域清单/去重归一化)待下一轮。**/changelog 顶部说明语下方、时间线上方加一张与版本卡片同款风格的入口卡(代码专属图标 +「技术日志」+「面向开发的技术变更记录」+ 进入箭头),从群友版更新公告可直达开发版技术日志。同步移除首页页脚的「技术日志」链接,避免入口冗余(index.astro)。dbc7252):deploy.yml 在 build 前新增 npm test 步骤,55 用例任一失败即中断部署防回归;node-version 20 → 24(测试直接 import .ts,依赖 Node 默认类型剥离 23.6+,与本地一致);package.json 补 engines.node >=23.6.0。已验:Actions run 全绿(Test/Build/Deploy 均 success)。astro check 门禁暂缓——会暴露既有类型缺口(如 RestaurantCard props)致 CI 红,需单独一轮类型修复后再上(HANDOVER 待办)。[2.3.1] - 2026-09-10
Security
363b61e+9a6cd50):此前 /api/supabase/[...path].ts 透传任意路径 + 注入 anon key,等同开放中继,任何人可匿名调 admin_* rpc、访问 /auth、/storage、任意表或 DELETE。新增 src/lib/proxy-guard.ts(decideProxyAccess 纯函数)收口为白名单:仅放行 GET restaurants/reviews/edit_log、POST restaurants/reviews + /rest/v1/rpc/increment_review_count、PATCH restaurants/reviews(仅软删 {deleted:true});其余(admin_* rpc、/auth/*、/storage/*、其它表/rpc、DELETE/PUT、非软删 PATCH)一律 403。判定前置于 env 检查,未配上游也不沦为开放中继。加每-IP 限流(读 120/分、写 30/分,超限 429 + Retry-After;内存级,强限流仍需 #5 的 CF Rate Limiting)。新增 tests/proxy-guard.test.mjs(13 用例覆盖放行/拦截矩阵),全套 55 单测通过。/rest/v1/rpc/(非 /rpc/);首版白名单误写导致 increment_review_count 被拦成 403(写评价后计数 +1 静默失败;因首页评价数已改实时聚合,用户可见数不受影响)。线上回归发现后即修正,并补测试守卫两种路径形式。[2.3.0] - 2026-09-10
> 本版本为一次安全/缺陷修复批次(对应 HANDOVER「技术债与改进审计」)+ 图片显示根治。回退基线 tag v2.2.0,每条修复独立 commit、可单独 git revert。
Security
/api/admin/restore-restaurant、/api/admin/restore-review(service_role + HMAC token);admin.astro 移除客户端口令 / sessionStorage / admin_* RPC 调用(anon 可执行、返回布尔的口令 oracle 客户端入口已除);delete-restaurant/delete-review 复用为硬删并清理过期 Storage 逻辑。线上已验:4 个端点无 token 一律 401。(B3 代理白名单尚未做,见审计待办)/api/upload 加固(审计 #7):同 IP 限流(10 次/分)+ 文件头魔数校验,不再轻信客户端声明的 MIME。/api/auth 登录限流(审计 #3 部分):同 IP 5 次/分,收敛在线爆破面。public/_headers(静态页)+ src/middleware.ts(SSR 页),补齐 CSP/X-Frame-Options 等,/admin 不再裸奔于点击劫持。Fixed
deleted=eq.false,notFound 返回真 404。根治「群友改动被静态 JSON 旧值静默盖回」及「编辑任意字段即清掉已标可外卖 d:true」。/img/ 代理,直接从 R2 binding(IMAGES)读对象返回、Cloudflare 边缘缓存;弃用 pub-*.r2.dev 公共开发域名直链(curl 200 但浏览器裂图)。新增 src/pages/img/[...key].ts(key 扁平校验防路径穿越、X-Content-Type-Options: nosniff、immutable 长缓存);upload.ts 返回 /img/、[slug].astro 的 proxyImage/proxyImg 统一改写,兼容历史四种来源,无需数据迁移。server-data.ts SYNONYMS 及对应单测同步。Changed
checkDeleteAllowed/incrementDeleteCount/DEL_THRESHOLD)、decodeServerError、window.__applyFilters、恒等 catMap、过期注释,收敛需维护的鉴权面。.gitignore 忽略 .wrangler/(本地 miniflare/wrangler 开发状态目录)。[2.2.0] - 2026-09-09
Added
/add 添加餐厅可直接勾选"支持外卖",详情页"编辑店信息"可修改,改动同样走 edit_log 审计、后台可采纳/撤销。外卖标记存于 ai_note.d(布尔),无需数据库迁移。edit_log 审计(改前/改后值、改动者、时间),管理后台 /admin 按餐厅分组逐笔"采纳/撤销"。后台仅在有待处理改动时显示修改记录,平时保持干净。formatPrice,卡片与详情页一律显示"人均X元",兼容纯数字/区间/文案等旧数据。Changed
src/data/updates.json(面向群友的友好文案,结构化)+ src/components/UpdateTimeline.astro(时间线 + 版本卡片 + 新增/优化/修复彩色标签);changelog.astro 改为渲染该模块,不再正则解析 CHANGELOG.md 原文(路由仍 /changelog,标题改「更新公告」)。首页头部新增「📢 更新公告」入口按钮 + 未读红点(localStorage.sf_updates_seen 记录已读版本,进入页面即标记已读)。**用户侧文案维护在 updates.json,技术细节仍记于本文件。**/tech-log):首页页脚入口,渲染 CHANGELOG.md 的技术变更记录;与群友版「更新公告」(/changelog,首页头部 📢)分层——一个给开发看技术细节,一个给群友看友好更新。/add 页分类由单选下拉改为多选勾选(第一个为主分类),与编辑弹窗/首页筛选一致。Fixed
edit_log 审计(后台可撤销)。[2.1.2] - 2026-08-14
Fixed
Security
admin_* 数据库函数密码已更换(旧值作废),新密码不落仓库明文,存于 Cloudflare secret / 安全记录。[2.1.1] - 2026-08-14
Fixed
随便/全球/有很多店 可以就近吃/西塔/全沈阳 等)归入其他/全区域/实际区,不再出现脏 tag。Added
src/pages/api/upload.ts:服务端 R2 图片上传端点(校验类型/≤8MB,返回公开直链)。[2.1.0] - 2026-08-14
Changed
restaurants.json 仅作数据库不可达时的首屏兜底。reviews 未删记录的实时聚合计数,不再读取可能失效的 review_count 字段。Added
/admin 可查看被隐藏数据,支持还原或彻底删除。Fixed
reviews.length 恒为 0 导致徽章不显示,已随首页 SSR 根治。overflow-hidden,消除长短不齐。/storage/...)与完整 URL 走代理,修复部分门店图片 404。Security
/api/admin/*:HMAC token(HttpOnly cookie)+ service_role + 每 IP 限流。src/lib/supabase.ts 的 PUBLIC_* 注入;移除各页硬编码 publishable key。scripts/scan-secrets.sh、.env.example。[2.0.0] - 2026-08-14
Added
avgPrice,卡片显示"人均 XX 元"。Changed
Security
esc() 转义(index / admin / 详情页)。/api/auth 端点),移除前端明文密码。Fixed
loadDynamic() 动态加载不再覆盖静态卡片的分类数据。Changed
ai_note 列存储结构化 tags + avgPrice JSON。Removed
src/styles/global.css(未引用的死代码)。src/lib/supabase.ts(未引用的死代码)。[1.0.0] - 2026-08-11
Added
[Unreleased]: https://github.com/voidleap-dev/xiaozhong-dianping/compare/v2.1.2...HEAD
[2.1.2]: https://github.com/voidleap-dev/xiaozhong-dianping/compare/v2.1.1...v2.1.2
[2.1.1]: https://github.com/voidleap-dev/xiaozhong-dianping/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/voidleap-dev/xiaozhong-dianping/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/voidleap-dev/xiaozhong-dianping/compare/v1.0.0...v2.0.0
[1.0.0]: https://github.com/voidleap-dev/xiaozhong-dianping/releases/tag/v1.0.0