火石营地管理系统是一套面向营地(含施工队考勤、营位出租)的轻量管理工具,提供 考勤记录、营位管理、用户与权限、服务器访问配置 等能力。后端采用 FastAPI + SQLite,前端提供三套界面,数据与逻辑保持一致。
桌面浏览器访问,管理员日常使用,功能最完整(含鸟瞰图、系统设置)。
手机浏览器打开,扫码/输入地址即用,支持考勤与营位。
编译为 App / 小程序,逻辑与 H5 一致,登录后先选功能再进页面。
技术栈:后端 FastAPI + uvicorn + SQLite(WebSocket 实时同步);前端原生 HTML / uni-app(Vue3)。启动后监听 0.0.0.0:<端口>,端口以「服务器IP管理」中配置为准。
| 一级模块 | 子模块 / 页面 | 核心功能 | 端别 |
|---|---|---|---|
| 主页面 | Dashboard | 当前在线用户(实时动态+端别)、历史记录、侧栏折叠 | 后台端 |
| 考勤管理 | 普通考勤 | 姓名+日期+时长+工作内容;增删改查/清空/复制/统计/详情;序号列可点击排序 | 三端 |
| 小时考勤 | 姓名+日期+时间段+工时;4 下拉自动算工时;序号列可点击排序 | 三端 | |
| 月度考勤 | 与普通/小时互相独立存储;标签为「备注」;序号列可点击排序 | 三端 | |
| 考勤看板 | 数据总览 | KPI 卡 + 三类考勤月份统计(前带「合计」总条数)+ 最近考勤(普通 / 小时 / 月度三类合并,列出 编号、姓名、日期、考勤类型、工作内容) | 后台端 |
| 营位管理 | 营位列表 | 卡片模式 / 列表模式 / 鸟瞰图模式三种视图切换;编号/姓名/状态/入住类型/电话/日期/金额/二维码/剩余天数;增删改查 | 三端 |
| 鸟瞰图 | 底图 + 营位图标定位;选状态→跟随图标→点击落位→写坐标;左键点图标看详情、右键弹出「移动 / 删除」菜单;图标大小按底图宽度等比缩放(三端比例一致,20%~300% 可调);底图变更实时同步到手机端;可下载底图原图(不缩放不转码,存档 / 备份用) | 后台端 / H5 | |
| 营位详情 | 左栏营位属性(对应数据库,实时剩余天数);右栏 两个页签:入住记录 / 变更记录·操作日志(点击切换并高亮) | 后台端 | |
| 到期自动释放 | 营位到期(退房日期已过)自动变回「空闲」并清空业务属性,保留编号/二维码/入住记录/鸟瞰图坐标;保存时退房日期就已过期的营位不会被自动释放 | 全端生效 | |
| 用户ID管理 | 账号与权限 | 生成用户ID+密码、设置模块权限、删除账号;顶部含超级管理员固定词条 | 后台端 |
| 系统设置 | 服务器IP管理 | 本地IP(只读)+端口+防火墙迁移;联网IP;测试连通 | 后台端 |
| 服务器访问地址 | 只读清单:5 种访问地址,端口随配置自动变 | 后台端 | |
| 测试连通 | 两步探测(TCP+本系统验证)本地/联网地址 | 后台端 | |
| 系统文档 | 内嵌本说明页(iframe),随项目变更同步更新 | 后台端 | |
| 删除的数据 | 回收站:考勤 / 营位两页,记录被删除的全部数据,可查看详情、恢复、彻底删除、一键清空 | 后台端 | |
| 系统初始化 | 顶栏按钮:凭管理员密码把全部业务数据清空到 0 数据状态,超管账号与用户ID均保留 | 后台端 |
| 约定 | 说明 |
|---|---|
| 侧栏可折叠 | 后台左侧菜单最下方有「⟨ 收起菜单 / ⟩ 展开菜单」按钮:点击后侧栏收缩为只留图标的窄条,再点一次展开。鼠标悬停在图标上会弹出该菜单的名称提示。折叠状态记在浏览器本地,下次打开保持。 |
| 侧栏图标对齐 | 所有菜单项统一为「固定宽度彩色圆形图标 + 菜单名」,图标与文字严格对齐;系统组的五个按钮同样对齐。考勤四项各有专属图标(仪表盘紫 / 普通绿 / 小时橙 / 月度蓝)。 |
| 弹窗风格 | 所有确认/提示使用居中对话框(替代浏览器原生 confirm / alert),遮罩点击 = 取消。详情类对话框采用「标题固定 + 右上角 ✕ 关闭 + 仅下方内容滚动」的版式。 |
| 序号 = 日期早晚 | 「编号 / 序号」列的值=该记录在当前列表里、按日期升序排出的位次:序号 1 永远是日期最早的那条,序号 N 是日期最晚的那条。所以「按序号排」与「按日期排」是同一件事 —— 点哪一列表头都只是切换升 / 降序,两列表头箭头永远同步;升序自上而下读 1…N,降序读 N…1。该值既不是数据库 id,也不是录入先后。 |
| 新增两段体验 | 新增数据后:① 列表框自动滚动到最顶端;② 新数据临时置顶 2 秒并高亮(此时序号列显示这条新数据自己的固有编号,第 10 条就显示 10),2 秒后回到它该在的位置。三端一致。 |
| 手机返回键 | H5 / APP 上按系统返回键时,若当前有弹窗则先关闭弹窗,不退出页面。优先级:二维码全屏 > 营位新增/编辑 > 考勤新增/编辑·详情 > 功能选择。 |
| 二维码下载 | 全屏查看电表二维码时,右下角 ⬇ 按钮保存到相册;未授权相册权限时会引导去系统设置开启;浏览器环境下退化为直接下载文件。 |
| 鸟瞰图全屏 | H5 / APP 的鸟瞰图可「⛶ 全屏」观看,支持 + / - 缩放、⟳ 旋转 90°、按住拖动(双指可捏合缩放);图片下沿叠了一排状态统计按钮(总计 / 已入住 / 空闲 / 已预订 / 维护),点一下只显示该状态的营位图标,再点一次取消。全屏里点营位图标会弹出营位详情对话框,按抬起时的坐标做命中测试,避免拖动 / 指针捕获把这次点击吃掉;详情关闭后仍留在全屏。 |
展示此刻已登录的移动端连接,每行显示使用人、用户ID,以及登录端别徽标(H5 / APP / 小程序)。下方实时滚动该用户的最新操作动态(新增/修改/删除考勤与营位)。
汇总登录、退出、新增、修改、删除、清空、改端口等事件,便于追溯。改数据会在动态中带上该条记录的编号(连续序号,非数据库 id)。
| 项 | 说明 |
|---|---|
| 条数不设上限 | 历史记录与实时操作动态不再限制显示条数(原来只显示 200 / 40 / 150 条)。所有登录、退出与数据变更记录永久保留、全量显示;列表内滚动,不截断。 |
| IP 显示 | 手机端会把「本次连接的服务器地址」随请求一起上报。后端统一解码并去掉协议头,只保留 host:port(如 192.168.1.2:8766),不再出现 http%3A%2F%2F… 这类乱码;历史里的旧记录也会在读取时自动纠正显示。 |
| 周期回填 | 服务启动时把数据库里的变更日志回填到实时操作动态,重启后动态面板不会空白。 |
KPI 卡 + 三类考勤的月份条数统计。月份统计条最前面是一个 「📊 合计」按钮,点击即显示该类型考勤所有月份的总条数(不用再逐个点月份累加)。
页面下方 「🕒 最近考勤」 表把 普通考勤 / 小时考勤 / 月度考勤三类合并到一张表,列为:编号、姓名、日期、考勤类型、工作内容;按日期倒序(同一天里新录入的在前),不设条数上限。
三种考勤模式共用相同的操作能力,仅字段略有差异。下表以「普通考勤」为例,小时/月度功能一致。
| 功能 | 说明 |
|---|---|
| 新增 | 填写姓名、日期、时长(整天/上午/下午/休班)、工作内容;保存即写入并广播实时动态。 |
| 编辑 | 修改当前这条记录(按编号定位);每次修改追加一条历史,历史记录全量保留(不再只留最近 10 条)。 |
| 删除 | 二次确认后删除单条记录;被删除的数据会进入「删除的数据」(回收站),可在系统模块中查看 / 恢复 / 彻底删除。 |
| 清空 | 清空当前模式全部记录(需确认);被清空的记录同样进入回收站,可恢复。 |
| 复制 | 把当前列表数据按「序号+Tab」复制到剪贴板,可直接粘进 Excel。 |
| 搜索 / 统计 | 按姓名/月份/日期筛选;统计卡显示记录数与总时长/总工时;月份 chip 显示各月条数(最前为「合计」)。 |
| 数据详情 | 点击某行查看完整字段与变更记录:新增即记为「初始数据」,之后的每次修改记为「修改」(列出修改前后值),Excel 批量导入的记为「批量导入」。 |
| 排序(编号 / 日期) | 表头的 「编号」与 「日期」两列都可点击切换「升序 ▲ / 降序 ▼」,三类考勤(普通 / 小时 / 月度)全部支持,三端一致,两列表头箭头永远同步。 序号与日期早晚一一对应:序号=该记录在日期升序序列里的位次,所以序号 1 就是日期最早的那条、序号 N 是日期最晚的那条。因此「按序号排」与「按日期排」是同一个顺序 —— 点「编号 / #」表头或点「日期」表头效果完全一样(都只是切换日期升 / 降序),升序自上而下读 1…N、降序读 N…1,不会再出现「序号和日期对不上」。 日期相同者按记录固有编号排列 —— ⚠️ 这个次序要跟着排序方向一起反向:实现上是「先排出唯一确定的升序序列(日期 → 固有编号),降序时把整条序列整体倒序」。若只把比较器里的「日期」分支反向、「同一天」仍固定按固有编号升序(2026-10-01 之前的写法),当天有多条记录时编号列就会读出 7,8,9,10,11,12,6,5,4,3 这样先升后降的断层。「忘了录某天、事后补录」的记录会自动落到它该在的日期位置。 |
| 新增置顶 2 秒 | 新增一条数据后,该条会先显示在列表第一位并高亮,约 2 秒后自动回到它该在的位置。 ⚠️ 置顶这 2 秒里,序号列显示的是「这条新数据自己的固有编号」(新加的是第 10 条就显示 10),不再是「1」;其余各行用不含这条新记录时的日期位次(1…N)顺延,不与置顶行撞号。2 秒后置顶解除,序号恢复为各自的位置序号。 |
| 列表框自动置顶 | 新增数据时列表框直接滚动到最顶端,置顶的那条数据即刻可见(三端一致)。 |
| 改造点 | 说明 |
|---|---|
| 字段精简 | 详情里不再显示「部门」「工时」两项(部门为历史遗留字段,工时对记录无意义)。 |
| 工号 → 编号 | 原「工号」改称 「编号」,并且显示的值就是该条的真实固有编号(第几条),与列表里的编号列完全对应,不再显示无意义的内部 ID。 |
| 关闭按钮移位 | 关闭 ✕ 从对话框右下角移到右上角,与标题同一行。 |
| 标题固定 | 滚动时只有下方的详情内容滚动,标题栏固定不动(含右上角关闭按钮),长内容也不会把关闭按钮滚出视野。 |
详情内容分三段:字段明细 → 🕓 变更记录(数据)(初始数据 / 修改 / 批量导入,全量保留)→ 📋 操作日志(谁在什么时间做了什么)。
字段:姓名 / 日期 / 时长 / 工作内容。工时按 整天=8h、上午下午=4h 计。
字段:姓名 / 日期 / 时间段 / 工时。时间段用 4 个下拉(起-止 时:分)自动换算工时。
独立表存储,字段同普通考勤,但「工作内容」列标签为「备注」。
营位列表页提供三套视图,切换按钮位于搜索框正下方(手机端在搜索框下一行,横排三个),当前视图高亮选中:
| 视图 | 说明 |
|---|---|
| 卡片模式 | 默认视图。每个营位一张卡片,展示编号、姓名、状态徽标、入住类型、联系电话、入住/退房日期、剩余天数、消费金额、电表二维码。 |
| 列表模式 | 一行一个营位,信息密度高于卡片,适合快速浏览与核对。字段与卡片保持一致:编号、姓名、状态徽标、联系电话、入住类型、入住/退房日期、剩余天数(已过期灰色、2 天内到期红色)、消费金额、备注;末列是 📷 全景图 / ✏️ 编辑 / 🗑️ 删除三个操作按钮。点整行打开营位详情。后台为真表格(表头吸顶),H5 / uni-app 为行式卡片(编号 + 姓名 + 状态在首行)。受搜索与状态卡筛选联动,权限与卡片一致(可编辑 / 删除 / 看全景图;后台「+ 新增营位」在列表模式同样可用)。 |
| 鸟瞰图模式 | 用一张营地地图做底图,把每个营位按其真实位置以彩色状态图标标注在地图上(详见「鸟瞰图」一节)。未上传底图时不显示任何营位图标,只在图片框里给一行浅灰提示,避免出现「中间孤零零一个图标」的观感。 |
| 手机端查看(H5 / uni-app) | 手机端在营位页搜索框下一行有横排的三个切换按钮(🗂️ 卡片 / 📋 列表 / 🗺️ 鸟瞰图),点「🗺️ 鸟瞰图」即可切换到鸟瞰图查看(只读):底图 + 彩色状态图标,点图标弹出该营位的详情对话框(编号 / 姓名 / 状态 / 入住类型 / 联系电话 / 入住·退房日期 / 剩余天数 / 消费金额 / 备注);点「🗂️ 卡片」「📋 列表」可随时切回。鸟瞰图查看视图里 右下角「+」按钮自动隐藏(新增 / 编辑 / 删除在卡片与列表模式可用);底图右上角有 「⛶ 全屏」 按钮,可全屏观看并旋转 / 放大 / 缩小 / 按住拖动(双指捏合);全屏里图片下沿叠了一排状态统计按钮(总计 / 已入住 / 空闲 / 已预订 / 维护,带数量),点一下只显示符合该状态的营位图标、其余隐藏,再点一次取消,选中的按钮高亮。手机端的搜索与状态筛选对鸟瞰图同样生效,后台换底图时手机端实时同步刷新。 |
营位工具栏只有「🧹 清空」「➕ 新增营位」两个按钮;原来的 「✥ 移动编辑」按钮已移除——移动与删除改为在鸟瞰图上右键点击营位图标弹出菜单完成。营位列表搜索框后面的「刷新」按钮已移除。
has_qr + 图片地址,卡片显示缩略图(长边 260px、约 18 KB、滚到才加载),点开全屏才下载原图(之后走浏览器缓存),接口响应从 3137 KB 降到 2 KB。图片的存库方式与画质完全没有变化,全屏查看、下载、编辑上传的老体验全部保留。新增 / 编辑营位的对话框字段顺序固定为:
编号 → 姓名 → 状态 → 入住类型 → 联系电话 → 入住日期 → 退房日期 → 消费金额 → 电表二维码 → 备注
| 状态 | 对话框显示的字段 | 保存后的数据 |
|---|---|---|
| 空闲 | 只显示 4 项:编号、姓名、状态、电表二维码(其余字段整行隐藏) | 除 编号 / 电表二维码 / 入住记录 / 鸟瞰图坐标 / 操作日志 外,其余属性(姓名、联系电话、入住类型、入住日期、退房日期、消费金额、备注、剩余天数)全部清空 |
| 已入住 / 已预订 / 维护 | 全部 10 项都显示 | 按填写的值保存;剩余天数根据退房日期实时换算 |
| 字段 | 对应数据库列 | 说明 |
|---|---|---|
| 编号 | code | 营位编号(如 A01)。唯一,未填编号时按姓名判重。 |
| 姓名 | name | 入住人 / 客户姓名。 |
| 状态 | status | 空闲 / 已入住 / 已预订 / 维护(四种)。 |
| 入住类型 | stay_type | 常驻 / 临时。空闲时留空。 |
| 联系电话 | phone | 入住人联系电话。 |
| 入住日期 / 退房日期 | checkin_date / checkout_date | 入住与退房日期。 |
| 消费金额 | amount | 金额(元)。 |
| 电表二维码 | qr_code | 手机拍的电表照片(动辄 1 MB 上下),以 base64 存库。⚠️ 列表 / 详情接口不再下发整段 base64(只回 has_qr + qr_url / qr_thumb_url),图片改走独立端点 GET /api/campsites/{id}/qr(加 ?thumb=1 出缩略图),带 ETag 缓存。详见「营位」章节「5. 电表二维码」。 |
| 备注 | description | 自由文本。 |
| 剩余天数 | 实时计算 | 退房日期 − 今天,由后端实时换算,不用库里的旧值:改退房日期立即跟着变,可显示「剩余 N 天 / 已到期 0 天 / 已超期 N 天」。 |
| 鸟瞰图坐标 | pos_x / pos_y | 营位在地图上的百分比坐标(0~100),与图片尺寸解耦,换图/缩放不错位。 |
| 图标大小(单个) | pin_scale | 该营位图标的缩放比例(20% ~ 300%)。为 null 表示「跟随全局默认」(ui_config.pin_scale)。清空为「空闲」时保留。 |
营位到期(状态为已入住 / 已预订,且退房日期已早于今天)时,系统自动把它变回「空闲」,无需人工操作:
| 项 | 说明 |
|---|---|
| 触发时机 | ① 每次读取营位列表 / 详情 / 统计时自动检查(30 秒内不重复扫描);② 后台每分钟兜底扫描一次;③ 服务启动时先检查一次(覆盖关机期间到期的营位)。 |
| 判定条件 | 状态 ∈ {已入住, 已预订} 且 退房日期 < 今天。 |
| 清空范围 | 姓名、联系电话、入住类型、入住日期、退房日期、消费金额、备注、剩余天数。 |
| 保留内容 | 编号、电表二维码、入住记录、鸟瞰图坐标(历史入住记录永久保留,不会被释放掉;营位在地图上的位置也不会丢)。 |
| 不释放的情况 | 保存时退房日期本身就已经过期(例如把历史入住补录成「已入住 + 退房日期是上个月」)时,系统视为人工确认要保留该状态,不会把它自动释放;保存后会弹窗提示原因。想让它变空闲:把状态改成「空闲」,或把退房日期改到今天之后。 |
| 前后端一致 | 释放后服务器会立刻广播一次同步,所有已打开的后台 / 手机端(卡片、鸟瞰图、正在看详情的对话框)一起刷新,不会再出现「卡片显示已入住、点开详情却已是空闲」。 |
| 留痕 | 自动写入一条 到期自动释放 操作日志,摘要含营位编号、原状态、退房日期,并逐属性列出「修改前 → 修改后」。 |
鸟瞰图界面中间是一张图片框,用于存放当前营地的地图;可上传 / 替换 / 清除底图(图片以 base64 存库,换人换电脑打开照样在)。
⚠️ 底图不跟接口一起传:底图是手机拍的照片(实测 2275×1280 ≈ 376 KB),若以 base64 塞在 GET /api/camp-map 的 JSON 里会膨胀到 502 KB,且完全无法被浏览器缓存——每次登录、每次切到鸟瞰图都要重下 500 KB。现在拆成两部分:GET /api/camp-map?slim=1 只回元信息(102 字节)+ 图片地址,图片本体走 GET /api/camp-map/image?v=内容哈希(二进制、带 ETag + max-age=31536000, immutable)。内容没变时浏览器直接用本地缓存,一个字节都不下载;换了底图 URL 里的 v 跟着变,旧缓存自动失效。不带 slim 调老接口仍会返回 image base64,保证没重新打包的旧 App 不会看不到鸟瞰图。三端登录成功后还会顺手预取一次底图(延迟 800ms,不抢首屏),等用户切到鸟瞰图时已经下好了。
按 Esc 可随时取消放置状态。
点「➕ 新增营位」后弹出的对话框分成两个页签,点击切换并高亮当前项:
| 页签 | 用途 |
|---|---|
| ➕ 新增营位 | 原有的流程:选一种营位状态(每种状态一个彩色小图标)→ 在地图上点击落位 → 弹出录入对话框 → 保存完成。 |
| 📍 已有营位 (带数量) | 列出还没有鸟瞰图坐标的营位(带编号 / 姓名 / 状态)。点其中一条 → 在地图上点击 → 只把这一个营位的坐标写回数据库(其它信息完全不动),适合先建了营位、后上传底图的情况。列表为空时提示「所有营位都已经在地图上定位好了」。 页签上的数字徽标= 待定位营位数量,列表上方也写明「共 N 个营位还没有鸟瞰图坐标」,新增 / 定位 / 编辑 / 删除后自动刷新。 |
在鸟瞰图空白处右键 → 「全屏显示」即可进入全屏观看(再在空白处右键 → 「退出全屏」,或点右上角按钮退出)。全屏里:
| 操作 | 效果 |
|---|---|
| 滚动滚轮 | 放大 / 缩小地图(0.5× ~ 8×),且以鼠标所在位置为原点——鼠标指着的那个点保持不动。右上角实时显示当前倍率(如 150%)。 |
| 按住左键拖动 | 平移地图。空白处、营位图标上都能拖;拖完的抬手动作不会误触发「落位」或「打开详情」。平移带夹取保护,地图不会被拖出屏幕外找不回来。 |
| ➕ 新增营位 | 右上角主按钮,与未全屏时功能完全一致:选状态 → 幽灵图标跟随鼠标 → 左键落位 → 录入表单。全屏不会被退出,对话框浮在全屏层之上。 |
| ⟲ 重置视图 | 一键回到 100% 居中。(每次重新进入全屏也会自动重置。) |
| 右键图标 / 左键点图标 | 与未全屏时完全一致:右键=移动 / 删除 / 调整图标大小;左键=营位详情。 |
鼠标左键点击图标=查看该营位详情;右键单击图标=弹出菜单,三个入口:
| 操作 | 交互方式 |
|---|---|
| ✥ 移动 | 图标进入跟随鼠标状态(虚线高亮):在地图上移动鼠标,图标跟着走;点击左键即把新坐标写入数据库。按 Esc 取消。 |
| 📏 调整图标大小 | 在菜单里就地展开一条上下滑动的竖直滑块条(20% ~ 300%),只改变「当前右键选中的那一个营位」的图标大小,其它营位不受影响。拖动时实时预览,松手才写库;下方还有「↺ 恢复默认大小」= 写回「跟随全局」。面板会说明当前状态:还没单独设过 → 「当前跟随全局 N%,拖动即单独设置」;已单独设过 → 「仅此营位(全局默认 N%)」。滑块的可取范围由服务端下发,与手机端读到的是同一个值。 |
| 🗑️ 删除 | 弹出确认对话框;确认后同步所有前端、后端与数据库(含该营位的入住记录),并进入回收站可恢复。 |
菜单会贴着鼠标出现,并在贴边时自动收进窗口内;点击别处、按 Esc 或滚动都能收起菜单。展开滑块后面板变高,菜单会自己重新贴边。
营位图标的直径 = 底图显示宽度 × 3.2% × 图标大小比例。后台、H5、uni-app 三端使用同一个比例常量、同一个计算公式、同一份缩放配置,所以同一个营位图标在后台大屏与手机小屏上「相对整张地图的大小」完全一致。
| 规则 | 取值 |
|---|---|
| 图标直径 | 底图显示宽度 × 3.2% × 图标大小比例(默认 100%),兜底 4 ~ 420 px;比例取自该营位自己的设置,没单独设过则用全局默认 |
| 圆内图标字号 | 直径 × 0.5(跟着图标一起缩放,不再写死 11 px) |
| 编号标签字号 | 直径 × 0.26,最低 6 px;胶囊内边距 1px 5px、圆角 5px、行高 1.2,编号始终显示。 2026-09-30 按用户反馈整体调小(原为 直径 × 0.3、最低 7.5 px、内边距 2px 9px):手机底图只有 ~364 px、图钉仅 ~11 px 时,旧的最小字号下限会把标签抬到 10 px —— 编号胶囊反而比图钉还宽、挡住地图。四端(admin .cm-pin .pin-code / 游客端 .pin .pc / H5 .cmap-pin .pc / uni-app PIN_LBL_RATIO+PIN_LBL_MIN)必须同值。 |
| 边框 | 2 px 白边含在直径内(box-sizing:border-box),不再额外把图标撑大 4 px |
ui_config.pin_scale(接口 GET/POST /api/ui-config,默认 100%,可调 20% ~ 300%)。campsites.pin_scale(接口 PUT /api/campsites/{id}/pin-scale,为 null 表示「跟随全局」)。在鸟瞰图里右键图标 →「📏 调整图标大小」,改的就是这一个营位;点「↺ 恢复默认大小」即写回「跟随全局」。后台上传 / 更换 / 清除底图,或拖动营位图标改变位置后,正在查看鸟瞰图的手机端(H5 / uni-app)会通过 WebSocket 立即重新拉取底图与坐标并重绘,无需手动刷新。
点击营位卡片或鸟瞰图上的营位图标,即打开该营位的详情对话框。版式为「左侧:营位全部信息 + 右侧:入住记录 + 变更记录 / 操作日志」,标题固定、右上角 ✕ 关闭、仅内容区滚动。
| 区域 | 内容 |
|---|---|
| 左栏 | 电表二维码 + 营位属性卡:编号、姓名、状态、入住类型、联系电话、入住日期、退房日期、剩余天数(自动换算)、消费金额、备注、鸟瞰图坐标。左栏只放营位信息,不再有多余的按钮。 |
| 右栏 | 顶部是两个页签:📜 入住记录 与 🕓 变更记录 / 操作日志。点击切换,当前页签高亮(白底 + 阴影 + 条数角标),两个页面各自独立、互不挤在同一页里;数据多时当前页面内部出现上下滚动条,不会把对话框撑高。 |
| 区块 | 位置 | 说明 |
|---|---|---|
| 📜 入住记录 | 右栏页签 1 | 该营位的全部入住记录时间线(按入住日期倒序,最新在前),每条显示「入住 ~ 退房 / 姓名 / 电话 · 消费」,可单条删除。 |
| 🕓 变更记录 / 操作日志 | 右栏页签 2 | 该营位的全部操作日志(逐属性「修改前 → 修改后」)。与入住记录分成两个页面,点页签切换、当前项高亮。 |
详情里的 📋 操作日志 记录每一次变更,且写得足够详细:
add)、修改(update)、状态变更(status)、到期自动释放(expire)、调整鸟瞰图位置(position)、上传二维码(qr)、新增/删除入住记录(stay_add / stay_del)、底图变更(map)、删除(delete)。(空),入住类型未设置时显示 (未设置),电表二维码显示 已上传(图片 1.51 MB)/ 无,金额显示 ¥ 前缀,保证一眼能看懂。data:image/png;base64,…)。为避免把整串图片数据写进日志、动态与网络消息导致页面卡顿,系统在写入日志前就把这类值压成概述(如「已上传(图片 1.51 MB)」「(图片数据 1.51 MB)」),超长文本一律截断为前 200 字 + 总字数;营位详情里的二维码图片、回收站恢复数据都完全不受影响。维护可登录移动端的账号,控制谁能看、谁能改。
| 项 | 说明 |
|---|---|
| 账号 | 固定账号 admin(超级管理员),存放在后台数据库独立表中。 |
| 初始密码 | 666888。页面顶部直接显示/编辑,可点「💾 修改密码」更新。 |
| 改密留痕 | 每次修改都会写入操作日志,明确标注是哪个账号的什么密码、原密码多少、修改后密码多少(形如「原密码 666888 → 新密码 abc999」)。 |
| 可否删除 | 不可删除 只能修改密码;即使执行「系统初始化」清空全部业务数据,该账号与密码也依然存在。 |
| 功能 | 说明 |
|---|---|
| 新增用户 | 点击「生成用户ID」得到唯一 ID,填写使用人、登录密码,勾选各模块权限(考勤/营位 可编辑 或 仅查看)。 |
| 设置权限 | 按模块设置「可编辑 / 仅查看」,角色(管理员/普通)由权限自动推导。 |
| 修改密码 | 后台可重置某用户的登录密码。 |
| 删除 | 移除账号,立即失效其登录与在线连接。 |
| 重复校验 | 用户ID 全局唯一,重复添加会被拒绝。 |
决定「前端该连哪个地址、哪个端口」。分两块,互不影响:
| 块 | 字段 | 说明 |
|---|---|---|
| 本地 IP | 本地IP(只读)+ 端口 | 本地IP 由程序实时检测、不可手填;端口可改。保存后:① 防火墙入站规则自动迁移到新端口;② 服务自动重启到新端口;③ 手机端地址提示同步更新。 |
| 联网 IP | 联网对外地址(整条 URL) | 填写内网穿透 / 公网映射后的地址(如 https://xxx.cpolar.cn),不影响本地配置。 |
一个只读清单(不落地、实时生成),把「手机端该怎么连」直接列出来:本机后台 1 行 + 局域网 4 行(考勤 / 营位 / APP 服务器地址 / 游客登记)+ 联网 5 行(后台 / APP 服务器地址 / 考勤 / 营位 / 游客登记)= 已配联网地址时 10 行(未配则 5 行),端口随「服务器IP管理」实时变化。点一下地址就在新标签页打开;要复制时用鼠标拖动选中地址文本即可(右键「复制链接地址」亦可)。
| 序号 | 名称 | 访问地址(示例) | 用途 |
|---|---|---|---|
| 1 | 本机后台管理 | http://127.0.0.1:端口/ | 仅本机浏览器 |
| 2 | 手机端·局域网·考勤 | http://局域网IP:端口/attendance/ | 同 WiFi 打卡 |
| 3 | 手机端·局域网·营位 | http://局域网IP:端口/campsite/ | 同 WiFi 看营位 |
| 4 | 手机端·联网·考勤 | 联网地址/attendance/ | 外网流量访问 |
| 5 | 手机端·联网·营位 | 联网地址/campsite/ | 外网流量访问 |
「服务器IP管理」每块各有一个「测试连通」按钮,点击后做两步探测:
结果用四色提示:连通(绿) / 端口未监听或拒绝(红) / 域名无法解析 / 隧道未指向本机(黄)。联网地址还会自动回退 HTTPS 探测。
系统级回收站,集中收录被删除的数据,避免误删造成不可逆损失。分两个页签:
| 页签 | 内容 |
|---|---|
| 考勤数据 | 普通考勤 / 小时考勤 / 月度考勤 三类被删除的记录 |
| 营位数据 | 被删除的营位记录 |
每条记录列出 原编号、关键字段、删除时间、删除人,并提供以下操作:
| 操作 | 说明 |
|---|---|
| 查看 | 打开详情:完整字段 + 删除时间 / 删除人 + 全部历史变更记录(含初始数据与每次修改)。 |
| 恢复 | 恢复到原来的序号位置。若该位置已被其它数据占用,会提示选择:覆盖该位置数据,或新增到末尾(编号重新计算)。无论哪种方式,历史变更记录都完整保留。 恢复成功后前端立即联动刷新:回收站列表、对应模块的数据表格(普通 / 小时 / 月度考勤 / 营位)、首页看板统计与历史记录会在同一时刻一起更新,不需要手动刷新页面。 |
| 彻底删除 | ⚠️ 不可恢复 从回收站永久移除。 |
| 清空 | 列表右上角的「🧹 清空」按钮,把回收站里的全部数据一次性彻底删除(有警告与二次确认)。 |
位置:后台端顶栏 右上角日期/时间左侧 的红色按钮「🧨 系统初始化」。用于把系统一键恢复到 0 数据状态。
| 项 | 说明 |
|---|---|
| 触发条件 | 必须输入 管理员密码(即超级管理员密码,初始 666888),密码错误直接拒绝。 |
| 清空范围 | 普通考勤、小时考勤、月度考勤、营位、营位入住记录、鸟瞰图底图、删除的数据(回收站)、操作日志、登录/退出历史。 (不含用户ID:账号与其管理权限会保留,初始化后仍可正常登录。) |
| 保留内容 | 用户ID 及其管理权限;超级管理员账号(admin)与登录密码;服务器 IP / 端口 / 联网地址配置(系统启动依赖)。 |
| 编号重置 | 自增序号归零,初始化后重新从「第 1 条」开始编号。 |
| 留痕 | 初始化动作本身会写入一条操作日志。 |
在对应的考勤页(普通考勤 / 小时考勤 / 月度考勤)点击工具栏的「导入 Excel」按钮,选择 .xlsx 或 .xls 文件。若文件含多个工作表,会先弹出工作表列表让你选择要导入的那一张(首行必须是表头)。
| 数据类型 | 必填列 | 可选列 | 取值要求 |
|---|---|---|---|
| 普通考勤 | 姓名、日期 | 时长、工作内容 | 时长 = 整天 / 上午 / 下午(缺省「整天」) |
| 月度考勤 | 姓名、日期 | 时长、备注 | 时长 = 整天 / 上午 / 下午 / 休班 |
| 小时考勤 | 姓名、日期 | 时间戳、时长(小时)、工作内容 | 时间戳如 09:00-12:00;时长留空时按时间戳自动换算 |
列名同时兼容英文表头(name / date / duration / hours / work_content),方便从其它系统直接导出后导入。
YYYY-MM-DD、时长取值非法 → 该行跳过并在结果里提示「第 N 行:具体原因」,其余合法行照常导入。完整的历史记录形态:新增 →「初始数据」;之后每次编辑 →「修改」(含修改前后值);批量导入 →「批量导入」;删除/恢复 → 见「删除的数据」模块。所有历史全量保留。
在考勤页工具栏点击「导出 Excel」,可选导出全部或指定月份;再选择保存路径与工作表名(已存在同名工作表则覆盖该表)。无法使用原生保存对话框时,浏览器会走下载兜底。
| 数据类型 | 导出工作表名 | 导出的列 |
|---|---|---|
| 普通考勤 | 普通考勤 | 姓名、日期、时长、工作内容 |
| 月度考勤 | 月度考勤 | 姓名、日期、时长、备注 |
| 小时考勤 | 小时考勤 | 姓名、日期、时间戳、时长(小时)、工作内容 |
导出同样会写入一条操作日志(「批量导出某类数据 N 条到 路径(工作表:XX)」)。导出的列与导入的必填/可选列一一对应,导出的文件可直接修改后重新导入,形成数据搬运闭环。
为防止脏数据,新增与修改均做了去重(后端强制 + 前端提示)。规则如下:
| 数据 | 判重字段 | 新增重复 | 修改撞到其它记录 |
|---|---|---|---|
| 普通考勤 | 姓名 + 日期 + 时长 | 拦截 | 拦截 |
| 月度考勤 | 姓名 + 日期 + 时长 | 拦截 | 拦截 |
| 小时考勤 | 姓名 + 日期 + 时间段 | 拦截 | 拦截 |
| 营位 | 编号(未填则姓名) | 拦截 | 拦截 |
| 用户ID | 用户ID(全局唯一) | 拦截 | 拦截 |
三端保存时都会读取后端的返回体判断是否报错,出现重复提示时不会关闭对话框,方便直接改;保存成功才关闭并刷新。
| 角色 | 能力 |
|---|---|
| 管理员 | 全部功能可用,含系统设置、用户管理、增删改数据、鸟瞰图、回收站。 |
| 普通用户 | 按分配的模块权限:可编辑 / 仅查看。仅查看时无写按钮,写操作会被后端拒绝(403)。 |
移动端(H5/小程序)登录时带上用户标识,后端写操作守卫据此放行或拦截;后台桌面端为管理员,不受限。营位的坐标调整、底图上传、入住记录增删同样纳入写权限保护。
手机/小程序填首页提示的 http://局域网IP:端口(如 http://192.168.1.2:9876),进入对应页面。注意:手机上填 127.0.0.1 指向的是手机自己,连不上后台。
在「服务器IP管理 → 联网 IP」填入内网穿透/公网地址后,手机用流量访问该地址的 /attendance/ 或 /campsite/。可用「测试连通」验证隧道是否指向本机。
| 项目 | 说明 |
|---|---|
| 需求 | 「帮我把 uniapp 前端最新改好的打包成 apk,库密码是 qcd666888」。 |
| 产物 | 火石营地uniAPP/火石营地.apk(17.44 MB,2026-10-02 16:39 云端打包成功)。打包前把上一版(10-01 22:05 那版)备份为 火石营地uniAPP/火石营地_旧.apk;另在项目外 D:\WorkBuddydata\_apk_backup\ 留了两份历史版本(10-01 22:05 / 10-01 22:01),需要回退随时可用。 |
| 打包方式 与证书 | HBuilderX 命令行云打包(cli pack --platform android),证书类型 Android 自有证书(D:\qcd.keystore,别名 qcd)——不是自定义调试基座。打包前先 keytool -list -v -keystore D:\qcd.keystore 用用户给的库密码验明正身,证书指纹 SHA1 77:94:7B:70:6F:1F:43:90:F8:22:4F:F1:C6:00:F0:A1:B4:E6:58:91 与手机上已装旧包完全一致 → 新包可直接覆盖安装、手机本地数据不丢。(keystore 密码不记录在项目文件里,需要时另行提供。) |
| 排队与耗时 | 提交时队列第 2 位(前面无在制任务,未弹「已有任务是否取消重排」的交互确认),全程 2 分 14 秒(16:37:50 提交 → 16:39:46 出下载地址)。 |
| 本次进包的 uni 端改动 | 本轮与上一轮改在 pages/index/index.vue 的功能点:① 鸟瞰图新增「⬇️ 底图」下载按钮(左上角,与右上角「⛶ 全屏」左右分列): saveCampMap() 走 GET /api/camp-map/download 取底图原图(不缩放不转码,与缩略图端点 /api/camp-map/image 严格分开);H5 走 a[download]、App 走 uni.downloadFile + 存相册,并挂 _t= 时间戳破缓存(后端 ETag 强缓存回 304 会被误判成失败),失败再退 arraybuffer 兜底。② 收到 campsite 同步时,连「正开着的营位详情」一起刷( onSync 补 refreshCampMedia(detail.value.id))—— 原来只刷营位列表,而全景图在详情对话框里(打开时单独拉的 media),所以后台 / H5 刚上传的照片要关掉详情再打开才看得见。 |
| 四项校验 | ① 合法 APK(856 个条目,zip 里有 AndroidManifest.xml);② 签名 SHA1 与旧包一致(77:94:7B:…:58:91);③ 真解析二进制 AndroidManifest(scripts/axml_manifest.py,不靠字符串 grep):package=uni.app.huoshi、versionCode=100、versionName=1.0.0、minSdk=21、targetSdk=28、debuggable=False(正式包,非调试基座);④ 改动真的进包(见下一行)。 |
| 新旧包对比 (最硬的证据) | 同一关键字在旧包装 0 次、新包 ≥1 次,才能说明这一版带来了什么: · assets/apps/__UNI__BD258C8/www/app-service.js:接口路径 /api/camp-map/download 旧 0 → 新 1;按钮文案 底图 旧 0 → 新 1。· pages/index/index.css:新增样式类 .cmap-dl 旧 0 → 新 1(原有 .cmap-fs 两版都是 1,作对照)。· 反向验证:H5 专属分支里的 营地鸟瞰图(a.download 的文件名)在 App 包里为 0 —— 正好证明 #ifdef H5 条件编译剥离正常,App 端走的是原生下载分支。⚠️ 计次数一律用 grep -o KW | wc -l:release 把 app-service.js 压成一行,grep -c 数的是匹配行数,任何关键字都会恒返回 1 或 0。另外 <script setup> 里的变量名(如 saveCampMap / refreshCampMedia)会被压缩重命名,grep 结果是 0,不代表改动没进包。 |
| 装机冒烟 (不只是「包对」,还要「能起来」) | 装到 MuMu 模拟器(emulator-5560)独立跑一遍:adb install -r 成功 → monkey 启动 → 前台 Activity 为 uni.app.huoshi/io.dcloud.PandoraEntryActivity(已脱离 HBuilderX 基座独立启动)→ 截图确认登录页完整渲染、无白屏(标题栏「登录」/ 火石营地 logo / 服务器地址 / 用户ID / 登录密码 / 使用人 / 自动登录 / 登录 / 测试连接全部在位)→ logcat 无 Uncaught / TypeError / FATAL。 |
| 说明 | 本轮只重新编译打包,未改动任何源码;后端 app.py、后台、H5 均不受影响。打包过程中 HBuilderX 提示「应用缺少隐私政策配置」,仅影响上架国内应用市场,自用安装无需处理;另有「未勾选 x86 CPU」警告,只影响夜神等 x86 模拟器,真机 / Arm 模拟器无影响。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「H5 前端上传全景图片还是慢,再优化一下,其他不变」。 |
| 先量再改 (实测结论) | 把这条路径整段拆开计时,找到三处真正的浪费: ① 竖图要解码两遍。上一版走 createImageBitmap(file,{resizeWidth}),遇竖图会「按宽缩到 1920 发现高还有 2560 超标」→ 再用 resizeHeight 把原图重解一遍。实测 12MP 竖图 566ms,而只解一次是 219ms(2.6×);横图 286ms → 196ms。② 档位比后端高,白传 1/3 字节。客户端压到 q0.90(12MP → 410KB),而后端 media_compress 收下后立刻按 PHOTO_JPEG_QUALITY=82 又编一次(279KB)—— 多出来的 130KB 属于纯浪费。③ 串行把两段时间相加。压缩是纯 CPU、上传是纯等待,一次选 6 张时总耗时 = Σ(压缩 + 上传)。 旁证(真实数据): media/campsite 里 13:51 那批 6 张照片的时间戳间隔是 6s / 15s / 6s / 6s / 8s,而服务端每张只存下 ~200KB —— 说明瓶颈不在网络,全花在手机端的解码 / 编码上。 |
| 提速① 档位对齐 | PANO_UP_Q 0.90 → 0.82,与后端 media_compress 的 PHOTO_MAX_SIDE=1920 / PHOTO_JPEG_QUALITY=82 完全对齐,客户端不再传后端马上就丢掉的字节。实测 12MP:请求体 394KB → 266KB(-32%),画质由后端同一档位决定,成品无差别。 |
| 提速② 只解码一次 | 新增 compressImageViaImg():<img> + URL.createObjectURL 一次解码 → naturalWidth/Height(天然按 EXIF 摆正)→ drawImage 一次性缩到目标画布 → 异步 toBlob。好处:不放大(Math.min(1,…))、不变形、不经 base64(省掉 1/3 内存与一次大字符串生成)、不再用同步的 toDataURL 卡主线程。任何一步失败回 null,调用方自动退回老路。二维码那条 data URL 快通道一行未动,避免动到已经稳定下来的扫码链路。 |
| 提速③ 流水线 | uploadCampMedia() 改成「上传第 i 张的同时,已经在压第 i+1 张」:prep(i+1) 在 POST 之前就启动。一次选 6 张时总耗时接近减半(串行是两段时间相加)。上传顺序、单张失败处理、后缀改 .jpg 的规则全部不变。 |
| 提速④ 看得见的进度 | 压缩一张原图要一两秒、批量更久,之前界面上一点反馈都没有,用户只能判断成「卡死」。现在上传中显示「正在上传 i/n …」、「正在压缩上传…」,结束显示「已上传 N 张全景图」。 |
| 其他 | 小图(<500KB)依旧一个字节都不压、原样上传;只压非 GIF 图片;压后统一 JPEG 且后缀同步改 .jpg(否则后端按扩展名判类型会不认);压不动 / 压完更大 / 根本不是图片 → 一律回退原文件,绝不让「压缩」把上传搞挂。后端与 uni 端一行未改。 |
| 验证 | ① 新增真 Chrome 端到端 _e2e/run_panofast.js(整库副本 + 临时端口 + 临时上传目录,真实数据零污染)27 项全过:档位 = 1920/0.82;压缩过程中 createImageBitmap 调用 0 次、FileReader 调用 0 次(用调用计数而不是计时来断言「单次解码、不经 base64」,不受机器快慢影响);竖图产物 1440×1920 没被拉横;真实上传请求体 原图 1.80MB → 272KB(0.151×);27MP 大图 → 1920 边长、0.086×;小图原样上传;createObjectURL 被打破后仍能退回老路产出合格 JPEG;上传中 / 完成的提示文案都在;9 次上传全部入库;0 个 JS 异常。② 两条反空转对照(防止断言只是「恰好成立」): compressImageFastBlob(老快路)在竖图上确实会调 createImageBitmap 两次;把「先压完再传」的串行版本跑一遍,压缩段与 POST 段的区间确实不重叠 —— 而流水线版本的事件序列是 comp,comp,post,comp,post,post(重叠成立)。③ 回归全过: run_panosync.js(21)、run_qrfast.js(34)、run_qrup.js(20)、run_camplist.js(后台 + H5)、run_access.js(56)、run_attsort.js(40)、run_panodl.js(28)、_qr_map_e2e.py(51)、_e2e/sfc_check.js(19 条守卫)、_frontcheck.py / _undeclared.py / _check_ui.py(只剩既有噪声)。④ 测试基准改为自己种:真实库里那张 1MB 老二维码已在 13:51 那次重启时被「存量收敛」压掉了(这正说明收敛在真实数据上生效), run_qrup.js / _qr_map_e2e.py 原先「拿库里真实大图当基准」的前提随之失效。已改为自己种一张 1800×2250 照片级 PNG 基准,不再依赖库里恰好有什么;_qr_map_e2e.py 里当兜底用的「纯随机色块」也换成照片级生成(随机噪点是 JPEG 最坏情况,实测只压到 1.68×,会把「缩到 1/4 以下」逼红)。⑤ 真实 camp_data.db 与 media/campsite 零写入。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「所有功能都可以了,就是 H5 前端上传二维码电表图片还是慢,这还是局域网,如果是外网的话那就慢的没法用了」。 |
| 先量再改 (实测结论) | 用真 Chrome 把 H5 这条上传路径拆开量了一遍,结论和直觉相反:压缩本身没问题(6.8MB 原图 → 446KB、浏览器内耗时 330ms、端到端 383ms),问题出在「压不动时的兜底」 —— 老写法是 FileReader 读成 base64 → 塞给 <img> 解码 → canvas → toDataURL(同步),手机拍的 12~48MP 原图会被整张解进内存(48MP ≈ 192MB 位图),中低端机上极易解码失败,而失败后代码是「退回原图」→ 于是把 5~27MB 的原图 base64 直接怼上网络。uni 端之所以不慢,是因为 chooseMedia(sizeType:['compressed']) 早就让系统压过一遍,H5 没有这一层。顺带查到:库里两个老营位的二维码是 1.42MB / 1.43MB 的 PNG 原图(老版本存进来的),每次打开营位弹窗 / 全屏二维码都要把这 1MB 拉一遍。 |
| H5 · 二维码 (快通道) | 换成按目标尺寸解码:createImageBitmap(file,{resizeWidth}) 直接解到目标宽,原图位图从不整张驻留;竖图会检测到「高已超目标」再用 resizeHeight 重新解(绝不先把小图放大);同步的 toDataURL 换成异步 toBlob。createImageBitmap 不可用、或任何一步失败 → 自动退回老路(实测回退路径产物同样合格)。 |
| H5 · 档位 (1400/0.88 → 1024/0.82) | 二维码是黑白线条图,1024px 按 300dpi 打印还有 8.6cm 宽、扫码绰绰有余,而产物小 2~4 倍(真实 1MB 原图:159KB → 97KB)。三端统一口径:H5 IMG_UP_MAX / IMG_UP_Q、后台同名字段、后端 QR_IMG_MAX_SIDE / QR_IMG_QUALITY 一起改。 |
| H5 · 防退化 (绝不静默发原图) | 新增 compressQrImage():快通道 → 老路 → 产物仍超过 QR_OK_MAX(900KB) 就用更狠的档位(768 / 0.78)再救一次 → 还不行就明确弹提示(「请先用手机『截图』,再上传截图」),而不是把几十 MB 的原图悄悄发出去。 |
| H5 · 模块图片 (原本一个字节都不压) | 自定义模块的图片字段走 pickModImage(),原来把原图直接 POST 给 /api/upload/file,一点没压(同属这一类问题)。现在接上同一套:压成 Blob(1400 / 0.85)再传,压后后缀同步改 .jpg,压不动回退原文件。 |
| 后端 · 存量收敛 (一次性、幂等) | 新增 shrink_legacy_qr_codes(),在 init_db() 时把 qr_code > 400KB 的存量行按写入路径同一套策略压回档位:只变小、压不动不写、不删任何东西、失败不影响启动。实测本机那两行 1.42MB → 130KB、1.43MB → 127KB(收紧 11 倍),都是 839×1024 的 JPEG。 |
| 其他 | 二维码仍存在库里(qr_code 字段)不变;三端读图路径 / 缩略图端点 / ETag 强缓存一律不动;uni 端一行未改(它上传的图会被服务端按新档位收敛,结果与 H5 一致)。 |
| 验证 | ① 新增真 Chrome 端到端 _e2e/run_qrfast.js(整库副本 + 临时端口,真实数据零污染)34 项全过:存量收敛 3 条(含库里真实的 1.36MB / 1.37MB 两行)全部 ≥9× 收紧且 ≤1024、二次读取幂等;后端 /api/upload/qr 产物 ≤1024 且是 JPEG;H5 快通道 6.78MB → 150KB、关掉 createImageBitmap 后仍产出 192KB / 1024、3MB 假图触发「不发出原图 + 明确提示」、真实上传路径 15.20MB → 请求体 151KB(137×)、模块图片 6.79MB → 378KB(18.4×)、0 个 JS 异常。② 回归全过: run_qrup.js(20)、run_panosync.js(21)、run_camplist.js(后台 + H5)、run_access.js(56)、run_attsort.js(40)、_qr_map_e2e.py(51)、_e2e/sfc_check.js(19 条守卫)、_frontcheck.py / _undeclared.py / _check_ui.py(只剩既有噪声)。③ _qr_map_e2e.py 与 run_qrup.js 里显式关掉了存量收敛(QR_IMG_LEGACY_BYTES = 1<<60)—— 它们要拿「库里那张原图级二维码」当压缩基准,收敛一开基准就没了。这属于测试前提过期,不是功能坏了。④ 真实 camp_data.db 与 media/campsite 零写入(测试全在副本库 + 临时上传目录)。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「uniapp 前端和后台上传全景图速度都可以,就是 H5 前端上传全景图的速度慢了,而且 H5 前端和后端上传完全景图之后 uniapp 前端没有实时更新显示出来,需要关闭再打开才能显示出来,再就是,Uniapp 前端和 H5 前端上传完全景图之后后台没有固定在全景图的页面而是跳到了入住记录页面,再就是 uniapp 前端的鸟瞰图地图的下载按钮在哪,没有的话就加上,其他功能不变」。 |
| H5 · 上传提速 (上传前先压) | 根因:H5 上传把手机原图(3~8MB)原样 POST(uni 的 chooseMedia 拿到的是系统压过的图、后台多在服务器本机传),所以只有 H5 慢。现在上传前先在浏览器里压:新增 _dataUrlToBlob() 与 compressImageFileToBlob(),档位 PANO_UP_MAX=1920 / PANO_UP_Q=0.90(与后端 compress_photo() 的 PHOTO_MAX_SIDE=1920 同档);只有 >500KB 的非 GIF 图片才压,压不动 / 压完更大就回退原文件(绝不让压缩把上传搞挂);压后统一是 JPEG,文件名后缀同步改 .jpg(后端按扩展名判类型)。实测 9.47MB 原图 → 上传请求体 1.94MB(0.205×)。原有 compressImageFile() 只是加了可选参数,二维码那边的调用点一行未改。 |
| uni · 实时刷新 (详情里的全景图) | 根因:onSync() 收到 campsite 同步时只刷新了营位列表,而全景图在详情对话框里(打开详情那一刻单独拉的 media)→ 列表刷新根本碰不到它,必须关掉详情重开才看得到。修法:同步分支里若详情正开着(detail.__site)就一并 refreshCampMedia(detail.value.id)。H5 端本来就有同类刷新,所以只有 uni 出问题。 |
| 后台 · 页签保持 (不再跳回入住记录) | 根因:详情弹窗会被实时同步反复整块重建,而重建出的 HTML 里「📜 入住记录」页签恒为选中态 → 一边看「📷 全景图」一边有人在手机上上传,页面就被弹回入住记录。修法:新增 campDetailTab 记住当前页签(showCampDetail(id, initTab) 记录、switchCampDetailTab(t) 更新),所有「刷新型」调用都带上它(同步刷新 / 保存鸟瞰图坐标后 / 编辑营位保存后 / 自己上传完后 / 删除全景图后,共 5 处);从卡片或鸟瞰图点图标打开详情仍落在「入住记录」,行为不变。 |
| uni · 鸟瞰图下载 (新增 ⬇️ 底图) | 后台(工具栏「⬇️ 下载底图」)与 H5(鸟瞰图左上角)本来就有,uni 一直没有 —— 现在鸟瞰图左上角新增「⬇️ 底图」(与右上角「⛶ 全屏」左右分列、不重叠),走 saveCampMap() 打 GET /api/camp-map/download 拿原图(不是页面那张压到 1600px 的显示图):H5 走 a[download],App 走 uni.downloadFile → 存相册,非 200 退回 request(arraybuffer) 自己写文件(与二维码 / 照片保存同一套)。 |
| 其他 | 后端一行未改(需要的接口早就有:POST /api/campsites/{id}/media、GET /api/camp-map/download)→ 本次不用重启后端,刷新页面即生效。照片长按保存、视频「保存视频」、三端下载入口、考勤与其它页面一律原样不动。 |
| 验证 | ① 新增真 Chrome 端到端 _e2e/run_panosync.js(自起隔离实例:整库副本 + 临时端口 + 临时上传目录,真实数据零污染)21 项全过:外部上传后后台仍停在「📷 全景图」且画廊 +1、停在「📜 入住记录」时同步也不会被弹走、大图上传请求体 9.47MB→1.94MB、压后 1920×1440 且是 JPEG、小图(104KB)原样上传、0 个 JS 异常。② _e2e/sfc_check.js 守卫 16 → 19 条(新增:鸟瞰图下载入口、下载打的是原图端点、同步时刷新正开着的详情),_e2e/guard_mutate.js 变异体 15 → 18 个(19 例)全被抓住(证明守卫非空转)。③ 回归全过: run_panodl.js(28)、run_access.js(56)、run_attsort.js(40)、run_camplist.js(后台 + H5)、_qr_map_e2e.py(51)、_frontcheck.py / _undeclared.py / _check_ui.py(只剩既有噪声)。④ 真实 camp_data.db 与 media/campsite 零写入(测试全在副本库 + 临时上传目录)。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「选择文本的不灵敏,改成左键单击地址可以访问,右键单击弹出复制和在其它浏览器中打开,如果选择后者就弹出电脑上现有的浏览器列表,选择进行访问」。 |
| 后台 · 地址列 (左键=打开) | 「系统 → 服务器IP管理 → 📡 服务器访问地址」表格地址列:左键单击=用浏览器打开(仍是 <a href target="_blank" rel="noopener">;桌面窗口里由 pywebview 交给系统默认浏览器打开,不会把后台窗口自己导航走)。⚠️ 上一版为了让用户能「拖动选中地址文本」把 user-select 设成了 text,结果单击时经常先被拖出选区、点起来不灵;现在改回 user-select:none —— 复制改由右键菜单承担,左键点击干脆利落。 |
| 后台 · 右键菜单 (复制 / 换浏览器) | 右键地址 → 弹出菜单 #caUrlMenu(复用鸟瞰图右键菜单样式,标题=该行名称、自动避开窗口边缘):📋 复制地址、🌐 在其它浏览器中打开。oncontextmenu 里 preventDefault(),不弹浏览器自带菜单;点击别处 / 按 Esc 自动收起。 |
| 后台 · 浏览器列表 (新增面板) | 点「在其它浏览器中打开」→ 弹出面板 #brPick,列出这台电脑上装的浏览器(图标 + 名称 + exe 路径,默认浏览器带「默认」标记并排第一条);挑一个 → 由后端用那个浏览器打开该地址,并提示「✅ 已用「X」打开」。没找到浏览器、或在本机以外访问后台时,给出对应说明并提示改用「复制地址」。 |
| 后端 · 两个新接口 | GET /api/local-browsers(枚举本机浏览器)与 POST /api/open-in-browser(用指定浏览器打开网址)。为什么必须放后端:网页里的 JS 受沙箱限制,既不能列出本机装了什么浏览器,也不能指定用哪个浏览器去打开网址( window.open / <a target="_blank"> 只能落到当前这个浏览器里)。枚举来源:注册表 Clients\StartMenuInternet(HKLM / HKCU / WOW6432Node)+ 常见安装路径兜底(Edge / Chrome / Firefox / 360 / QQ / 搜狗 / IE),按 exe 去重;默认浏览器读 UrlAssociations\http\UserChoice 并排最前;结果缓存 30 秒。安全:接口只收「浏览器编号」+「网址」—— 真实 exe 路径由后端自己重新枚举得到(绝不接受前端传来的路径 / 命令行),且网址必须命中本服务自己生成的那份地址清单,非管理员账号调用被拒。 另加 HS_E2E_DRY_OPEN=1 开关(仅端到端测试用):只回报「将要启动哪个浏览器」,不真的弹窗口。 |
| 后端 · 桌面窗口 | main() 里显式写下 webview.settings['OPEN_EXTERNAL_LINKS_IN_BROWSER'] = True:pywebview 对 target="_blank" 的处理默认就是「交给系统默认浏览器打开」;若为假,它会改用 load_url() 把当前窗口直接导航走(点一下「手机端 · 考勤」地址,后台窗口就变成手机版页面,退不回来)。默认值本就是 True,显写出来是为了将来升级 pywebview 改了默认值时不会悄悄退回那种行为。 |
| 其他 | 表格四列结构、地址生成规则(_access_rows())、copyText(用户ID 等处的点击复制)、copyCaUrl(保留未删)、首页顶部两行提示全部原样不动;H5 / uni-app / 游客端本次未涉及。 |
| 验证 | ① 真 Chrome 端到端 _e2e/run_access.js(本轮改造成自起隔离实例:整库副本 + 临时端口 + HS_E2E_DRY_OPEN=1,真实数据零污染、绝不真的弹出浏览器)56 项全过:左键仍是 <a target="_blank"> + rel=noopener、元素上无 onclick、user-select 为 none、每行 data-ca-no 齐全、右键事件被 preventDefault;真鼠标右键弹出菜单且标题=该行名称、两项文案正确、菜单不出窗口;点「复制地址」→ 菜单收起 + 复制到的正是该行地址 + 「已复制」提示;点「在其它浏览器中打开」→ 面板列出 4 个浏览器(与接口逐条一致、默认浏览器排最前且只有一个「默认」标记、exe 都是绝对路径)→ 选一个 → 面板关闭 + 「✅ 已用「360极速浏览器X」打开」;服务端校验:清单外的网址 / 不存在的浏览器编号都被拒;真鼠标点一下地址确实新开标签且目标就是该 href、原页面不被导航走;Esc 收起菜单与面板;全程 0 个 JS 异常。② 回归: _e2e/run_attsort.js(40 项)、_e2e/run_panodl.js(28 项)、_frontcheck.py(ADMIN / MOBILE 均 JS_OK)、_undeclared.py、_check_ui.py 全部照旧通过,无新增告警。③ 真实库 camp_data.db 与 media/campsite 零写入(测试全在副本库 + 临时端口)。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「把后台的服务器访问的地址改成点击可以访问,也可以选择地址文本,其他不变」——这一列原来是「点一下就复制」:想打开得先复制、再粘进地址栏,多一步。 |
| 后台 · 服务器访问地址 (改进) | 「系统 → 服务器IP管理 → 📡 服务器访问地址」表格里的地址由 <span> 改为 <a>:点一下就在新标签页打开该地址(target="_blank" + rel="noopener noreferrer",桌面窗口里则由系统默认浏览器打开);同时给该列显式加 user-select:text,地址文本可以用鼠标拖动选中,右键「复制链接地址」也照常。原来的「点击复制」不再占用点击事件。 |
| 后台 · 说明文字 | 该区块下方提示由「点击地址可复制」改为「点击地址可直接打开;需要复制时,用鼠标拖动选中地址文本即可(右键「复制链接地址」亦可)」,与实际行为一致。 |
| 其他 | 表格结构(序号 / 名称 / 访问地址 / 备注 四列)、地址生成规则、copyText(用户ID 等处的点击复制)与 copyCaUrl 全部原样保留:未删任何函数、未动其它页面与后端。 |
| 验证 | ① 真 Chrome 端到端 _e2e/run_access.js(本轮顺带修好:侧栏选择器还写着早已改名的 serveraccess,且「所有地址都以 / 结尾」与后来的 APP 裸地址行冲突 —— 修前 11 项 FAIL、与本轮改动无关):现在 31 项全过 —— 地址列全是 <a>、href 与 /api/client-access 逐条一致、target=_blank + rel=noopener、元素上已无 onclick、user-select 为 text、程序化选中能取到完整地址文本、真鼠标点一下确实新开一个标签页且目标就是该 href、原页面没被导航走、说明文字已更新、copyText/copyCaUrl 仍在、0 个 JS 异常;② 回归: _frontcheck.py(ADMIN / MOBILE 均 JS_OK)、_undeclared.py、_check_ui.py、_e2e/run_attsort.js(admin + H5 共 40 项)全部照旧通过,无新增告警;③ 全程在真实运行的 9876 上复验(前端 HTML 读盘即时生效,后端无改动、无需重启)。 |
| 项目 | 说明 |
|---|---|
| 需求 | 「加个下载全景图里面的图片和视频的功能,其他功能不变」——以前全景图里的照片只能放大看、视频只能播放,两者都拿不下来(想在电脑上存档、或转发给别人做不到)。 |
| 后端 | 新增 GET /api/campsites/{site_id}/media/{mid}/download:回文件本体,并带 Content-Disposition: attachment,文件名 = 营位<编号>_全景图|视频_YYYYMMDDHHMM.扩展名(中文名走 RFC 5987 的 filename*,ASCII 名做兜底)。为什么不直接把 /media/xxx 丢给 <a href>:静态目录是「在线预览」语义,点一下只会把图片 / 视频摊在新页面里;而且跨域时浏览器会直接忽略 download 属性(H5 用隧道 / 别的域名访问就是这么个情况),只有附件响应头才可靠触发下载。媒体 id 与营位对不上、或文件已不在 → 回 {"error":...}(HTTP 200,沿用本项目约定,前端判 d.error)。 |
| 后台 / H5 | 每张缩略图左上角加一枚「⬇ 下载」(与右上角的「✕ 删除」分列两角,避免误点);照片灯箱底部加「⬇ 下载原图」;全屏视频播放器左上角加「⬇ 下载」。 实现 downloadCampMedia(siteId, mid):fetch 附件端点 → blob → a[download] —— 这样能拿到后端给的中文文件名,失败(404 / 网络)只弹一句提示、不会把整个页面导航到一段 JSON 上;个别不认 blob 下载的内置浏览器退回新窗口打开该地址。回收站里那种只有 /media 地址、没有媒体 id 的历史照片,下载按钮会自动隐藏(点了没有意义)。 |
| uni-app | 视频全屏播放器底部加「⬇ 保存视频」(uni.downloadFile → uni.saveVideoToPhotosAlbum;H5 平台走 a[download];本次会话已缓存过的直接读本地文件,零下载秒存)。照片本来就支持:全屏看大图时长按即弹出「保存图片到相册」(这是之前定下的交互,本轮刻意未改动);没有用 uni.getFileSystemManager() —— App 端(js 引擎版)不支持它、回调永不触发,正是二维码上传那次的真根因。 |
| 验证 | ① 后端隔离回归 _qr_map_e2e.py 加第 8 组:照片 / 视频上传后都能下载、Content-Type 对、attachment + 中文 filename* 对、文件后缀与实际落盘一致、媒体 / 营位对不上时回 error(37 → 51 项全过);② 新写真 Chrome 端到端 _e2e/run_panodl.js(整库副本 + 临时端口 + 临时上传目录,真实数据零污染):后台登录 → 打开营位「全景图」→ 点 ⬇ 真的落盘(文件名是中文、字节数与服务端一致)→ 灯箱 / 全屏播放器同样能下 → 后端出错不落盘只提示 → H5 画廊同样能下 → 0 个 JS 异常(28 项全过);③ _e2e/sfc_check.js 静态守卫 14 → 16 条(新增:防「⬇ 保存视频」入口被删、防保存实现改回 App 端不支持的 API),并用 _e2e/guard_mutate.js 做变异测试证明守卫非空转(13 → 15 个变异体,15/15 全部被抓住);④ 回归: _frontcheck.py、_undeclared.py、_check_ui.py、uni SFC 真编译全部照旧通过。 |
| 踩坑留痕 | ① 无头 Chrome 对同名文件是直接覆盖(不像有头版会改成「名字 (1)」)—— 端到端里「再下一次同一张照片」的断言会因此假失败,改成每个阶段先清空下载目录; ② Chrome 默认只放行同一来源的一次自动下载,之后的会被静默拦掉 → 脚本启动前预写 Preferences 把 automatic_downloads 设为允许;③ 本机沙箱下 fs.cpSync 会让 node 直接以 127 退出、一行输出都没有,改用 copyFileSync 逐个拷;④ node 的 process.exit() 在 Windows 管道下会截断报告(整份输出一行都打不出来),改用 process.exitCode;⑤ 隔离实例若没置 set_server_started(True),/api/campsites 会被数据门控挡成空列表(页面看起来「什么都没有」);⑥ 只改 DB_PATH 不改 MEDIA_DIR 时,副本库里指向真实文件的全景图记录会「记录在、文件找不到」→ 临时上传目录要先把真实 media/campsite 拷一份进去。 |
| 项目 | 说明 |
|---|---|
| 起因 | ① 希望后台能下载鸟瞰图底图(存档 / 备份);② uni-app 前端添加 / 更换电表二维码保存不了;③ H5 前端添加营位点保存「卡死」,要过很久才反应过来并提示「网络不好」。 |
| 根因 (②③ 同一个) | 三端上传二维码走的都是「把手机原图整个读成 base64 再塞进 JSON」,而且全程没有任何压缩。实测库里真实的两张二维码就是 1888×2304 / 1792×2208 的 PNG,单张 base64 1.36 MB;经手机 WiFi 上传要几十秒 —— H5 等不下去就报「网络不好」,uni App 端请求超时后错误又被 catch(() => {}) 静默吞掉,表现就是「保存不了二维码」。另一个放大器:保存接口的响应把刚提交的整段 base64 原样回传,等于又下一次 1.4 MB(前端其实只用到 d.error)。后端本身不是瓶颈:2.5 MB 的二维码入库 + 写日志 + 广播,实测只要 0.35 秒。 |
| 修法一 上传前压缩 | 三端各加一个 compressImageFile():读图后把图片缩到最长边 1400 px、转 JPEG 质量 0.88(二维码 1400 px 仍足够扫)。实测真实二维码 1.07 MB → 217 KB,压缩 4.9 倍。解不开的图(如 iPhone 的 HEIC)自动退回原图;透明底先垫白再转 JPEG(否则会变黑);压完反而更大时用原图。落点:后台 onCampQr()、H5 pickQr() / onQrSiteFile()、uni pickImage()。 |
| 追修 uni 端 (同一轮) | H5 / 后台修好后,uniapp 端仍然传不上去 —— 单独定位到两个只在 App 端成立的坑,这也正是「H5 行、uni 不行」的原因: ① compressImage 的 quality 只对 jpg 有效(uni 与 5+ 文档均明写「仅对jpg格式图片有效」)—— 库里那条真实二维码是 1792×2208 的 PNG,对它压 quality 等于什么都没压;② 只传 quality 不会缩尺寸(width / compressedWidth 是 App 3.0.0+ / 3.7.0+ 才有的参数,旧写法根本没传)→ 手机 4000×3000 原图压完质量仍近 1 MB。两条叠加,uni 端实际仍要上传 1401 KB 的 data URL(同图 H5 走 canvas 真缩图只有 217 KB)。 修法:改成「先量尺寸 → 只缩不放(最长边缩到 1400 px)→ 一律转 JPEG」—— App 端首选 plus.zip.compressImage(width:'1400px' + format:'jpg',5+ 默认输出即 jpg),失败或非 App 端退回 uni.compressImage(带 compressedWidth),再失败才用原图;压缩产物以 .jpg 结尾,读图时按后缀判 mime 才不会把 JPEG 标成 png。实测同一张真实二维码 1401 KB → 168 KB,缩小 8.3 倍。另:压缩后仍大于 900 KB 会弹提示把体积数字报出来,避免下次再遇到时只看到「网络不好」四个字; plus.zip 属 5+ 默认内置模块,无需在 manifest 里额外声明。 |
| 追修② uni 读图 (真根因) | 上面那轮改完、APK 也重新打包之后,uni 端依旧传不上去。这次没有再猜 —— 直接查后端日志,结论很硬: ① change_logs 里所有成功的二维码上传全部来自 H5(隧道)与后台(127.0.0.1),APP 一次都没有过;② APP 的写请求长这样:「修改营位:024;(无属性变化)」、 detail.fields = {} —— 说明二维码压根没进请求体,所以根本不是「上传超时」,而是选完图什么都没发生。真正的原因: uni.getFileSystemManager() 在 uni-app js 引擎版 App 端根本不支持。官方文档原文:「app平台,目前 uni-app x 支持本 API…但 uni-app js 引擎版还不支持,可部分替代的方案是 plus.io」。旧代码(包括上一轮我写的那版)读图这一步都直接调它 → App 端拿到 undefined/抛异常 → 读图回调永远不会触发,选了图也没有下文。而 H5 端走的是浏览器 FileReader,所以「H5 一直好、只有 App 不行」与这个原因完全吻合。修法:抽出 readFileAsDataUrl() 按平台分流 —— App 走 plus.io.resolveLocalFileSystemURL → entry.file → new plus.io.FileReader().readAsDataURL()(5+ 原生 API,App 端稳定);H5 走 uni.request 取 arraybuffer 再 arrayBufferToBase64(H5 同样没有 getFileSystemManager);小程序才用 getFileSystemManager。顺带把两处「静默」补上: uni.chooseImage 的 fail: () => {} 改为把原因说出来(相册权限被拒时不再「点了没反应」);每次选完图提示一下实际体积,下次一眼就能看出压缩到底有没有生效。 |
| 追修③ App 改直传 (最终方案) | 上面第二轮改完(读图改走 plus.io)之后,用户依旧反馈「选完二维码没反应」,而时间戳又一次对上了:change_logs 最新那条(21:54)仍是「修改营位:024 ;(无属性变化)」、detail.fields = {} —— 二维码还是没进请求体;同时 access_logs 显示 App 走的是 局域网 192.168.1.2:9876,根本不是网络问题。不再在「怎么把本地图片读成 base64」这条路上打转 —— 那条路在 App 端每一步( getImageInfo → plus.zip.compressImage → plus.io 读文件 → base64)都可能不回调,任何一环断掉,用户看到的就是「没反应」。改成用本项目里已经验证可用的通道(营位全景图一直就是这么传的):App 端 uni.chooseMedia / uni.chooseImage 选图 → uni.uploadFile 把文件本身 POST 到既有的 POST /api/upload/qr(multipart)→ 后端转成 data URL 返回 → 再走原来的保存链路。好处:① 不读本地文件、不转 base64,彻底绕开 App 端不支持的几个 API;② 二进制直传,没有 base64 膨胀;③ 压缩统一交给后端 shrink_qr_data_url()(该端点顺带接上了压缩)。兜底:万一 multipart 通道不可用,自动退回「本机读图( plus.io)」那条老路,两条腿走路;且该端点在当前运行中的后端上就已经可用(实测 0.03 秒返回 data URL),不必等重启。 |
| 修法二 后端兜底压缩 | 新增 shrink_qr_data_url():三个写入口(新增营位 / 编辑营位 / 单独上传二维码)写库前统一再过一道(超过 1400 px 或大于 300 KB 才压,EXIF 方向先摆正)。这样即使前端没压、或历史数据里已经存了大图,库里也不会再囤几 MB 的 BLOB。同时给这三个接口的响应瘦身(只回 has_qr + qr_url),不再把刚提交的 base64 原样传回去。 |
| 修法三 不再静默失败 | uni 端保存营位 / 上传二维码原本是 .catch(() => {}),失败毫无提示。现改为弹出具体原因(api.friendlyError 会把「连接超时 / 连接被拒绝」等翻译成中文),并在提交期间显示「保存中… / 上传中…」遮罩,避免用户以为没反应而反复点。 |
| 新增 下载底图 | 后端新增 GET /api/camp-map/download:返回用户当初上传的原图(不缩放、不转码,与页面显示用的 1600 px 压缩版是两个东西),带 Content-Disposition(中文文件名走 RFC 5987 的 filename*)。入口:后台鸟瞰图工具栏「⬇️ 下载底图」(排在「上传 / 更换地图」之后)、H5 鸟瞰图左上角「⬇️ 底图」(有底图才显示)。前端用 fetch + blob 而不是 <a href>:万一拿不到图只提示一下,不会把整个页面导航走。桌面窗口是 pywebview,下载依赖 ALLOW_DOWNLOADS=True(此前已置好)—— 改的是宿主,需重启桌面程序。 |
| 验证 | ① 隔离回归 _qr_map_e2e.py(真实库临时副本)37 项全过:用库里那张真实的 1.36 MB 二维码做基准,入库体积缩到 1/4 以下(实测 6.5×)、最长边 ≤ 1400、仍是可解析图片;三个写入口一致;小图 / 非 data URL 不被破坏;响应体 < 20 KB;底图下载字节与原图完全一致、带 Content-Disposition、无底图时 404。第 7 组专测「App 端 multipart 直传」:把手机原图级的 PNG 直接 POST 给 /api/upload/qr,返回的 data URL 已经是压缩过的(1.36 MB → 0.21 MB)、最长边 ≤ 1400、并且能直接落库(「选图 → 落库」整条链路打通)、Content-Type 不标准时按扩展名兜底不退化。② 真 Chrome 端到端 _e2e/run_qrup.js(整库副本 + 独立端口)20 项全过:后台与 H5 用真实二维码跑压缩 —— 1.07 MB → 217 KB、JPEG、最长边 1400;后台点「⬇️ 下载底图」真的落盘且大小 = 原图字节数;H5 按钮与函数就位;全程 0 个 JS 异常。③ 变异测试: _qr_map_mutate.py(把后端压缩改成原样返回)→ 5 项 FAIL;_qrup_mutate.py(把前端压缩改成直接交出原图)→ 6 项 FAIL(含「体积显著变小」「最长边 ≤ 1400」),证明断言不是空转,且跑完自动还原源文件。④ 回归: run_attsort.js(admin + H5 共 40 项)、_frontcheck.py、_undeclared.py、_check_ui.py、uni sfc_check.js(含 14 条崩溃 / 排序 / 上传压缩 / App 读图 / App 直传守卫)全部照旧通过;守卫本身用 _e2e/guard_mutate.js 的 13 个变异体验证过「非空转」(新增「App 端读图整段删掉 plus.io 分支」「选图 fail 改回空函数」「App 端二维码整段删掉 multipart 直传」三条)。 |
| 结论 | 后台 / H5 刷新即生效;uni 端需重新打包 APK(本次改的是 pages/index/index.vue 的 pickImage;若用 HBuilderX「真机运行」则直接重跑即可);后端改了 POST /api/upload/qr(接上压缩),重启即生效 —— 但即使不重启,该端点也能正常返回 data URL,只是回传的体积不压缩。 |
| 项目 | 说明 |
|---|---|
| 起因 | 用户反馈:① 在「用户ID管理」里给账号勾上「可编辑」后点保存,列表里仍显示「仅查看」;② 希望「系统初始化」清库时保留用户ID(账号与各自的管理权限一起留着)。 |
| 权限保存 不生效(根因) | 后台「用户ID管理」页上有两个权限勾选框容器 —— 新建用户表单 #newPermBox 与「修改权限」弹窗 #permChipsBox;但复选框当时写的是固定 id(perm_attendance / perm_campsite)。document.getElementById() 只返回文档里第一个同名元素(新建表单那个),于是:· 打开弹窗时,回显勾选被写到了新建表单上,弹窗里的复选框全空; · 在弹窗里勾完点保存, readPermsFromBox('permChipsBox') 读到的却是新建表单的状态(未勾)→ 提交上去永远是 read → 存完还是「仅查看」。⚠️ 这是个「看起来完全正常、保存结果却恒错」的静默 bug(不报错、控制台干净),只有把两个容器放在同一页面里才会触发。 |
| 修法 (前端) | 复选框不再写 id,改由各自容器内定位:renderPermChecks() 渲染后用 box.querySelectorAll('input[type=checkbox]') 按顺序回显,readPermsFromBox() 同样只在传入的容器里取值。两个容器从此完全隔离,谁也不会读到对方的状态。 |
| 防静默 降级(后端) | PUT /api/user-ids/{id}/permission 里,外来 perms 中未提交的模块 key 原本固定按 user 兜底(norm_perms(perms, "user"))。若前端因「模块清单尚未加载完」而少提交某个模块,该模块会被判成「仅查看」,把管理员静默降级。改为沿用该账号当前角色兜底(管理员 → 缺失模块补「可编辑」),与 norm_perms 里既有的「新模块不降级」约定保持一致。 |
| 系统初始化 保留用户ID | 清空表清单 RESET_TABLES 移除 user_ids:初始化仍清空全部业务数据(普通/小时/月度考勤、营位、入住记录、鸟瞰图底图、回收站、操作日志、登录历史),但用户ID 及其管理权限完整保留(与超管账号、服务器 IP/端口配置同列「初始化不清」)。弹窗文案同步改成「用户ID 及其管理权限会保留」,toast 改为「业务数据已清空(用户ID已保留)」。初始化后原账号仍可正常登录,不必把所有使用人账号重建一遍。 |
| 验证 | ① 隔离回归 _perm_e2e.py(真实库临时副本)19 项全过:权限保存为全 write → 角色推导为 admin、回读与列表接口一致;新增自定义模块后只提交老两个模块 key 也不降级;初始化后考勤归零、用户ID 与权限原样保留、原账号仍可登录。② 真实 Chrome 端到端 _e2e/run_perm.js(整库副本 + 独立端口,真实数据零污染)20 项全过:登录 → 进「用户ID管理」→ 打开「修改权限」→ 勾选 → 保存 →列表 chips 变「可编辑」且带 👑 管理员、接口回读 role=admin;反向取消一项后正确回落到「仅查看」;两个勾选框容器互不污染;全程 0 个 JS 异常。③ 变异测试 _e2e/perm_mutate.js:把实现改回旧的「固定 id + getElementById」写法再跑一遍 —— 8 项 FAIL(列表仍是「仅查看」、接口 role=user),与用户报的现象逐条对上,证明断言不是空转。 |
| 项目 | 说明 |
|---|---|
| 起因 | ① H5 端(/campsite/)的鸟瞰图全屏要与 uni 端一致:跟随手机横屏自动显示、去掉「⟳ 旋转」按钮、横屏时「⛶ 全屏返回」与 5 个状态按钮一起贴右上角、退出全屏回到竖屏常态;② 录入的考勤要按月份内日期自动排列——忘了录某一天、事后补上,也能自动落到对应的日期位置,并且序号必须和日期早晚对得上(点序号排序后不能出现「序号和日期不一样」);③ 后台点「⬇ 下载二维码」没反应(不报错、不弹框、控制台干净);④ 鸟瞰图里营位图标正下方的编号太大、挡住地图;⑤ 新增数据在第一位停留那 2 秒里,序号显示成「1」不对 —— 应显示这条新数据自己的编号(新加的是第 10 条就显示 10);⑥ 点排序后编号列出现「先升后降」的断层 —— 用户当天(10-01)录了 6 条,降序时编号列读成 7,8,9,10,11,12,6,5,4,3。 |
| H5 全屏 跟随横屏 | 删掉工具排里的 ⟳ 按钮与其 fsRotate() 函数。新增 isFsLand() / autoFsRot() / fsApplyImgBox() / onFsViewportChange() / lockOrientation():· 自动定向 autoFsRot():手机横屏且底图本身是竖图才转 90°(让长边对上屏幕长边、横屏铺满),其余情况保持 0° —— 竖屏下的显示与以前点开时完全一致;· 可用尺寸对调 fsApplyImgBox():转 90° 后「图片的宽」占屏幕的高、「图片的高」占屏幕的宽,所以内联 max-width/max-height 也要换(90vh / 94vw)。不换的话会继续被 CSS 里的 max-height:80vh 压住,转完了还是小得铺不满;· 按钮贴右上角:横屏时 #fsView.fs-land .fs-tools 上移到 max(12px, safe-area-inset-top)(横屏没有状态栏),状态条 placeFsStats() 改成 justify-content:flex-end+右端让开工具排 → 两者同一排、整排在右上角;· 退出复位: closeCampFs() 把 fsRot/fsScale/fsTx/fsTy 清零、清掉内联上限、移除 fs-land;openCampFs()/closeCampFs() 分别 lockOrientation('auto') / ('portrait');· 方向监听: resize + orientationchange(后者延后 140ms 再量,部分浏览器早于 innerWidth 更新),只在方向真的翻转时才重置缩放,避免地址栏伸缩把视图顶掉。 |
| H5 竖屏 锁定说明 | 页面是纯浏览器页(/campsite/),网页无权强制竖屏:screen.orientation.lock() 需要全屏权限、非全屏下必然 reject,已 .catch() 静默。实际表现=跟随手机方向,手机转回竖屏页面即自动恢复竖屏布局,不会卡在横屏或转着。若该页被 App 内嵌运行,会自动走 plus.screen.lockOrientation 真正锁方向(同一函数内分支判断)。 |
| 考勤默认 按日期排列 | admin / H5 / uni 三端的考勤表格(普通 / 小时 / 月度)默认排序列从「编号(录入顺序)」改为 日期升序(SORT_BY / TAB_STATE.*.sortBy / tabState.*.sortBy 由 'no' → 'date')。同一月份里按日期先后排,补录的记录会自动落到该日期该在的位置,而不是一直沉在表格末尾。想按录入先后看,点「编号 / #」表头即可切回。admin 三个表的静态表头箭头也同步改成「编号 ⇅ / 日期 ▲」,避免登录前显示错箭头。 |
| 考勤序号 与日期一一对应 | 表里「编号 / 序号」列的值=该记录在当前列表里、按日期升序排出的位次(不是行下标、不是数据库 id、也不是录入先后),因此 序号 1 永远对应日期最早的那条、序号 N 对应日期最晚的那条。 · 按日期升序 → 自上而下读 1…N;按日期降序 → 自上而下读 N…1; · 「点编号表头」与「点日期表头」是同一件事 —— 都只是切换升 / 降序(两列表头共用同一个方向、箭头永远同步),不会再出现「点序号排序后日期是乱的、序号和日期对不上」; · 记录被搜索 / 月份筛选挡住、不在当前列表里 → 退回固有编号兜底; · 新增记录「置顶 2 秒」期间是唯一例外:置顶那一条显示它自己的固有编号(第 10 条 → 10),其余行用「不含新记录」的日期位次顺延、不撞号(详见下一行)。 实现:admin 渲染前先 setDateSeq(key, filtered) 把筛选后的集合按日期升序算出 DATE_SEQ[key](id→序号),noOf() / noFor() 都查它,点表头统一走 flipSortDir(key) 翻转方向(toggleSort 与 toggleDateSort 现在只是它的两个别名);H5 用 dateAscMap(shown)(displayNoMap() 返回它)+ flipAttSortDir();uni 用 dateSeqMap computed + flipAttSortDir(),两列表头图标都由 sortArrow 派生。三端的详情弹窗与复制取同一口径,不会再出现「表格显示第 3 条、点进去写着 #214」:admin 用 noFor(key,id)、H5 用 _attShownNo、uni 直接用 noOf()。注意:序号是「当前所选月份 / 搜索结果」范围内的序号,所以选中某个「月份」筛选项后,序号会在该月内从 1 重新排。 |
| 同一天多条 · 降序断层修复 | 用户反馈「点排序,编号列不对」—— 截图里编号列自上而下是 7,8,9,10,11,12,6,5,4,3。根因:排序比较器把「日期」与「同一天」写成了两条独立规则 —— 日期部分按方向( dir==='desc' ? 1 : -1)反向,同一天却固定按记录固有编号升序((a.id||0)-(b.id||0),与方向无关)。用户 10-01 那天有 6 条记录(日期位次 7…12),降序时这 6 条仍按升序排、其余日期已倒序 → 编号列先升后降。这不是显示错,是排序顺序本身自相矛盾。改法:把「按方向排」改成「先排出唯一确定的升序序列(日期 → 固有编号),降序时把整条序列整体 reverse()」。这样编号列与日期列成为严格的互逆排列:升序恒 1…N、降序恒 N…1,任何一天有多少条都不会断层。落点:admin applySort()、H5 sortAttList()、uni shown computed(三处比较器结构一致,一次改齐)。实测(真实数据):直接连用户正在跑的 9876 服务(只读)读回 12 条真实考勤 —— 升序 1…12 / 日期 09-16…10-01;点一次排序后降序 12…1 / 日期 10-01×6 → 09-30 → … → 09-16,逐行严格 −1,无断层。⚠️ uni 端改动需要重新打包 APK 才在手机上生效。 |
| 新增置顶 2 秒 显示固有编号 | 用户反馈:新增数据在第一位停留那 2 秒里,序号显示成「1」不对 —— 应该显示这条新数据自己的编号(是新加的第 10 条就显示 10)。 改法(三端同口径):置顶期间① 置顶那一条 → 取固有编号(按 id 升序连续号 = 它是第几条);② 其余各行 → 取不含这条新记录时的日期位次。于是新记录日期落在中间时也不会出现「置顶行是 3、下面某行也是 3」的撞号,2 秒后归位时其余各行的序号原样不变。 落点:admin noOf() / noFor() 加置顶分支、setDateSeq() 在置顶期间把新记录排除出日期序列(一处集中,三个 render 无需改);H5 displayNoMap() 在置顶期间返回「排除新记录后的 dateAscMap」并把新记录那一项改写为它的固有编号(原先置顶期间直接 return null、调用处退化成行下标 i+1);uni dateSeqMap computed 同样排除新记录、noOf() 加置顶分支(原 flashing computed 已删)。详情弹窗 / 复制自动跟随(admin noFor、H5 _attShownNo、uni noOf),置顶那 2 秒内点开新记录详情,显示的也是它的固有编号。 |
| 后台二维码 下载修复 | ⚠️ 根因在宿主而非前端:桌面后台是 pywebview 窗口(任务管理器里 pythonw.exe + 一串 msedgewebview2.exe),而 pywebview 的 webview.settings['ALLOW_DOWNLOADS'] 默认 False,它的 edgechromium 后端收到 WebView2 的 DownloadStarting 时直接 args.Cancel = True —— 下载被静默取消,网页侧拿不到任何错误。修法: app.py 的 main() 里、webview.start() 之前置 webview.settings['ALLOW_DOWNLOADS'] = True(settings 是 ImmutableDict,只能改已存在的键)。改后点下载弹系统「另存为」对话框,文件名取自 <a download="…">。前端 downloadQr() 一行没改(浏览器里本来就是直接下载)。改的是宿主代码,必须重启桌面程序才生效。 |
| App 崩溃 修复 | ⚠️ 真机(HBuilderX 运行日志)报 Uncaught TypeError: Cannot read properties of null (reading 'getBoundingClientRect') at uni-app-view.umd.js。根因不是 video 组件,而是把 scroll-into-view / 选择器指向了已从 DOM 销毁的节点 —— uni-app 视图层内部 querySelector 得到 null 再调 getBoundingClientRect,于是在框架里异步抛未捕获异常,页面里的 try/catch 根本拦不住。复现路径(确定可复现):先点营位卡片的「📷 全景图」( openSitePano() 把 scrollTarget 置为 'panoAnchor'),之后随便点开一条考勤详情 —— 详情弹窗被 v-if 重建,而考勤记录没有 __site,原本带 v-if 的 #panoAnchor 不在 DOM 里 → 崩。而 showDetail() / closeDetail() 当时都没有复位 scrollTarget,这个「坏 id」就一直挂着。修法(三层):① #panoAnchor 改为常驻空 <view>(不再挂 v-if),真正的内容块自己按 __site 判断;② showDetail() 与 closeDetail() 都复位 scrollTarget='',只有「📷 全景图」入口当次临时置位;③ 同一类隐患 measureFsTools()(查 .fs-tools,而它随 v-if="fsOpen" 消失)补上 入口 fsOpen 守卫+可取消的 scheduleMeasureFsTools() / cancelMeasureFsTools()(原先是裸 setTimeout(measureFsTools, 60),用户在这 60ms 内退出全屏或转屏就会打到已销毁的节点),closeCampFs() 与 onUnload() 都取消定时器。防回归: _e2e/sfc_check.js 新增 6 条静态守卫(锚点无 v-if、两处复位、fsOpen 入口守卫、定时器成对、禁止裸 setTimeout;后来连同 2 条排序守卫共 8 条),并用变异测试(_e2e/guard_mutate.js)确认这些守卫在把 bug「改回去」时真的会 FAIL(非空转)。另:日志里的 [JS Framework] Failed to receiveTasks, instance (9) is not available 是 HBuilderX「同步手机端程序文件成功」后旧页面实例被销毁产生的,属热更新副产物,与业务代码无关。H5 与后台(纯浏览器页)的同名调用都已有 null 守卫(if(!row||!st) return; / if(!tools) return; 等),无同类隐患。⚠️ uni 端改动需要重新打包 APK 才在手机上生效。 |
| 营位编号 胶囊整体调小 | 用户反馈「鸟瞰图里营位图标下面的编号太大、挡住地图」。 根因:编号胶囊的字号是 max(下限, 图钉直径 × 比例),而下限才是真凶 —— 手机底图仅 ~364 px 宽、图钉只有 ~11 px(全局图标大小 50% 时仅 6 px),旧下限 10 px 直接把字号顶到 10 px,胶囊(44×17)比图钉本身还宽 3~4 倍,整张地图上全是绿牌子。改法(四端同值,一次性收紧):字号比例 .32 → .26、下限 10 px → 6 px、胶囊内边距 2px 9px → 1px 5px、圆角 9px → 5px、行高 1.35 → 1.2;admin 的拖动预览 .cm-ghost .gh-code 一并同步。落点四处:admin .cm-pin .pin-code(frontend/index.html)、游客端 .pin .pc(游客入住信息自助录入/index.html,注释里本就写着「与后台一致」)、H5 .cmap-pin .pc(火石营地移动端/index.html)、uni PIN_LBL_RATIO/PIN_LBL_MIN + .cmap-pin .pc(pages/index/index.vue,字号由 pinTxtStyleOf() 内联成比例)。实测(真 Chrome + CDP,手机 390×844 视口、全局图标大小 50%):编号胶囊 44×17 → 27×11.2(面积约减 60%),字号 10 → 6 px,仍始终显示、仍与后台同款深绿胶囊。 ⚠️ 这四处必须同值,只改一端会出现「手机小了、后台还大」的割裂。 |
| 验证 | ① _e2e/run_h5_fs3.js(真实 Chrome + CDP Emulation.setDeviceMetricsOverride 模拟 390×844 / 844×390 转屏)13 项全过:工具排只剩「全屏返回」、竖屏 fsRot=0 且状态条在工具排下方、横屏 fsRot=90 / 有 fs-land / 状态条与工具排同排靠右紧贴 / 整排贴右上角 / 旋转后底图视觉宽 624px(视口 844px)、退出全屏全部复位、全程无 JS 报错;② _e2e/run_attsort.js(admin + H5,注入「先录 9/29、后补录 9/19」的真实形态)40 项全过:补录记录自动排到 09-19 之前位、序号与日期一一对应(09-19→1 / 09-29→2)、详情弹窗口径与表格一致(noFor 得 1 / 2)、点「编号 / #」表头切成日期降序(09-29 在前、序号 2)、再点回日期升序、点「日期」表头与点「编号」完全等价、两列表头箭头同步且无 ⇅ 残留、新增置顶 2 秒置顶行显示它自己的固有编号(第 3 条 → 3,不是 1)且其余行位次 1 / 2 顺延不撞号、置顶期间详情弹窗同口径、2 秒后自动归位序号回到位次、全程无 JS 报错;③ _frontcheck.py ADMIN / MOBILE 均 JS_OK、标签配平 226/226,uni _e2e/sfc_check.js 真编译 ALL OK + 8 条崩溃 / 排序防护静态检查全过,_undeclared.py H5 / UNI 段 OK(ADMIN 段报的 oncontextmenu 是内联属性的误报,onStageContext / onPinContext 均已定义);④ 二维码下载:同源探针实测 ALLOW_DOWNLOADS=0 → 无任何对话框(复现「没反应」),=1 → 弹 #32770「另存为」(下载已放行);⑤ 序号与日期是否真对应:注入 5 条乱序日期(录入顺序 9/25、9/10、9/30、9/05、9/18,其中 9/05、9/18 是事后补录)后在 admin / H5 上逐次点击表头、读回真实 DOM —— 升序 1/09-05、2/09-10、3/09-18、4/09-25、5/09-30(序号 1 正是最早那天),降序 5/09-30、4/09-25、3/09-18、2/09-10、1/09-05,点「编号」与点「日期」得到同一结果(SORT_BY 恒为 date);⑥ App 崩溃修复: _e2e/sfc_check.js 的 6 条守卫做过变异测试(新增 _e2e/guard_mutate.js,把 v-if 锚点 / 漏复位 / 去掉 fsOpen 守卫 / 裸 setTimeout 逐个改回去)—— 6/6 变异体都被抓住、5/5 变异体确认真的改动了源文件,证明断言不是空转(该脚本后来加入 2 条排序守卫,现为 7/7,见 ⑩);_frontcheck.py 原先硬编码 node 版本目录(22.22.2-3,已被运行时升级删掉)导致 FileNotFoundError,改为自动扫描最新版本目录;⑦ 编号胶囊调小: _e2e/run_pinratio.js 新增「编号胶囊尺寸」断言(字号必须等于 max(6, 图钉直径 × .26);且胶囊不许比图钉胖太多),在后台桌面视口 100% / 300% 与手机 390×844 视口的 100% / 300% / 50% 共 5 个场景实测,全部通过:手机端 27×11.2(字号 6)、后台 100% 27×14(字号 8.32)、300% 57×34(字号 24.96);并用 变异测试(新增 _e2e/pinlabel_mutate.js,往页面里注入旧样式)证明旧值会被抓住:字号 10 ≠ 6、胶囊 42×19.5 双双 FAIL,非空转;⑧ 顺手修了两条长期假红的断言: run_pinratio.js 的「直径 = 底图宽 × 全局缩放比」在真实库里会被单营位 pin_scale 覆盖(增量库中营位 000 单独设过 60%,内联 --pin 盖掉了画布级),导致后台侧 5 项无端变红、且「后台与手机比例一致」3 项跟着红。已在测试里只在内存中清掉单营位覆盖量(不写库),7 项红 → PIN_RATIO_E2E_ALL_PASS;⑨ 新增置顶序号:同一个页面里用「日期落在中间」的新记录(id 999 → 固有编号 3、日期位次 2,两种口径数字不同、能真正区分)实测 —— 置顶期间表格读出 3/09-25、1/09-19、2/09-29,2 秒后归位读出 1/09-19、2/09-25、3/09-29,admin 与 H5 两端一致;并用 变异测试(新增 _e2e/flashno_mutate.js,在页面内把 noOf / setDateSeq(H5 为 displayNoMap)覆盖回旧实现)证明旧行为会被抓住:基线 pass、变异后读出 1/09-25、2/09-19、3/09-29 断言 FAIL,非空转;⑩ 同一天多条降序断层: _e2e/run_attsort.js 新增「真实形态」用例(照抄 /api/attendance 的返回顺序:12 条,其中 10-01 一天 6 条)—— admin + H5 两端升序读出 1,2,…,12、日期 09-16…10-01,降序读出 12,11,…,1、日期 10-01×6 → 09-30 → … → 09-16,且「编号 −1」与「日期不增」逐行同向,40 项全过;并用 变异测试(新增 _e2e/samedate_mutate.js,在页面内把 applySort / sortAttList 覆盖回旧比较器)证明旧行为会被抓住:基线读出 12…1 通过、变异后读出 7,8,9,10,11,12,6,5,4,3,2,1(=用户截图那一串)断言 FAIL,非空转;uni 端跑不了无头,改由 sfc_check.js 两条静态守卫兜(shown 降序必须 arr.reverse()、比较器里不许再有内联 dir 分支),并纳入 guard_mutate.js —— 7/7 变异体全被抓住;⑪ 真实数据复验:用真 Chrome 只读连用户正在跑的 9876 服务,读回 12 条真实考勤并点一次排序 —— 升序 1…12 / 日期 09-16…10-01,降序 12…1 / 日期 10-01×6 → … → 09-16,逐行严格 −1、无断层。 |
| 项目 | 说明 |
|---|---|
| 起因 | ① 游客端(/guest)鸟瞰图的放大缩小也换成「单指拖动 · 双指缩放」(去掉 +/- 按钮);② 游客点击空闲营位时,营位编号后面再加「营位尺寸」和「全景图」,能看清营位具体环境与大小;③ 模块管理添加区按「选择主模块」下拉制重构(可新建主模块并配图标);④ 新增功能模块位置调节;⑤ 删除模块后主模块下拉没同步删掉的 bug;⑥ 更新提示里的「重启」措辞全部改成「同步」。 |
| 游客端手势 | +/- 缩放按钮移除。触屏:双指捏合以两指中点为锚连续缩放(1~4 倍,touchmove 拦掉原生滚动/页面缩放)、单指拖动=原生滚动平移;电脑:滚轮以鼠标位置为原点缩放、左键拖动平移(拖动后用 mMoved 拦掉补发的 click,避免误选营位)。.canvas 去掉 width 过渡动画,缩放即时生效、图标尺寸同步重算。图例加提示「单指拖动 · 双指缩放 · 点图标看营位」。 |
| 游客端 营位详情 | 点图标 / 下拉 / 未定位编号选中后,「已选择营位」条变为 编号 + 📐 营位尺寸 + 📷 全景图按钮。点「全景图」弹详情:编号 / 尺寸 / 状态(空闲·可入住)+ 全景图区(照片三列网格、点开进灯箱放大;视频内联播放)。数据来自 GET /api/campsites/{id}/media(即后台「营位全景图」上传的照片/视频),游客只读、换营位丢弃旧响应。 |
| 模块管理 主模块制 | 添加区不再有「模块层级」单选:第一行就是「选择主模块」下拉(每项带图标:内置组固定 🏠📅⛺🗑️、自定义主模块用其图标),选「+ 新建主模块名称」才显示「名称输入 + 图标」两行(图标只为主模块用);点「+ 添加到列表」直接创建一级主模块(fields 为空)并自动选中。选好主模块后填「功能名称」与字段保存(小标题「添加功能」、按钮「💾 保存功能」),新功能挂在该主模块下。 |
| 位置调节 | modules 表加 ord 排序列(旧库自动迁移并按创建顺序回填),列表 ORDER BY ord,id;新接口 POST /api/modules/{key}/move(body {dir:up|down},一级在一级之间、二级同组内两两交换 ord,到顶/底返回 moved:false)。后台「自定义模块管理」的已添加列表每行加 ↑ / ↓ 按钮;侧栏菜单与三端模块顺序跟随(移动会广播 module_updated,手机端弹同步提示)。 |
| 下拉同步 删除修复 | loadModulesForPerms() 里统一 rebuildModGroupSel()+onModGroupSelChange()——删除模块(含主模块)后,「选择主模块」下拉立即减项,不再残留已删名称。 |
| 措辞统一 | H5 / uni 的「新功能更新」弹窗:按钮「立即重启」→「立即同步」、提示改「后台调整了功能模块,有新功能更新。是否立即同步?」(删除场景「同步后即生效」);行为不变(重新加载拉最新模块清单)。后台两处 toast 同步改(「手机端同步后即可看到」「已通知各前端同步」)。 |
| 另含此前 未记入批次 | 关窗即关服务器(webview closed 事件 → 停 uvicorn + 复位 server_started + os._exit(0));三端对话框「误点遮罩先确认再关」;鸟瞰图全屏手势(单指拖动/双指缩放/图钉竖排/横屏一排);营位尺寸(size)字段四端打通;uni WS 全局单例收不到 auth_ok 的修复(doRestart 先 closeWs)。 |
| 验证 | run_guest.js 游客端真实浏览器回归 46 项全过(setZoom 代替已删按钮;新增断言:+/- 已移除、详情入口存在;⚠️ 期望值改为按单营位 pin_scale 现算——营位 000 单独设过 60%,旧测试写死全局 50% 会误报);_mod_e2e.py 22 项、move 接口隔离回归 13 项、_frontcheck.py、uni SFC 真编译全绿。 |
| 项目 | 说明 |
|---|---|
| 起因 | 要求:① 服务器启动做成 Windows 窗口样式(不要 cmd 黑框);② 后台「系统」菜单加「启动服务」入口;③ 登录后所有数据为空,点「启动服务器」才显示;④「收起菜单」上方加「添加模块」按钮,可动态建模块与字段(必填/选填);⑤ 新模块推送给 H5 / uni,弹「新功能更新,是否重启」;⑥ 重启后新模块出现在「选择功能」对话框并可增删改查。随后反馈:点启动提示成功但状态仍「未启动」、添加模块后前端找不到、H5 考勤页有上下滚动条、启动小窗口想并进后台窗口。 |
| 窗口化启动 | 新增 启动.pyw(pythonw 无控制台拉起 app.py,双击即出后台窗口);start.py 改为排错入口(保留控制台看报错);start.bat 转调 启动.pyw。独立的启动小窗口已取消——「正在启动,请稍候… → 服务器启动成功」改为后台页面内部浮层(#bootLayer),启动成功后自动进入正常后台页面。 |
| 数据门控 | 新表 system_state + 接口 GET /api/system/state、POST /api/system/start|stop(只开关数据、uvicorn 进程常驻)。登录后 routeAfterLogin() 未启动则只显示「启动服务」页(隐藏侧边栏与其它页面);点「停止服务器」同样回到该页、其它模块全部隐藏。未启动时业务列表 GET 一律返回空(_GATED_EMPTY,含营位媒体与 /api/modules/<key>/data),H5 / uni 同样受控。 |
| 添加模块 | 侧边栏「收起菜单」上方新增「+ 添加模块」。字段类型:单行文本 / 多行文本 / 数字 / 日期(带「清空日期」按钮)/ 单选下拉(可配选项)/ 复选开关 / 图片文件,均可设必填。三表 modules / module_data / system_state;接口 /api/modules… 与通用上传 /api/upload/file(存 media/…/modules/)。权限动态并入:reload_perm_modules() 把自定义模块加进 PERM_MODULES,写守卫与用户ID权限勾选即时生效;norm_perms 对新增模块 key 沿用角色默认(管理员=可编辑 / 普通=仅查看),避免新模块把老管理员降级。 |
| 前端通知与重启 | 后端广播 sys_event(server_started / server_stopped / module_added / module_updated / module_deleted,模块类带 needRestart)。H5 收到模块事件弹「新功能更新,是否重启」→ 确认后 location.reload();uni 由 api.js 转发 sys_event → uni.showModal → uni.reLaunch。重登后新模块出现在「选择功能」对话框,H5 / uni 均有通用列表 + 表单(按字段动态渲染,图片走上传)。 |
| 关键修复 | ⚠️ get_db() 只负责关连接、不自动提交——新增的 system_state / modules / module_data 写操作此前全部漏 conn.commit(),事务在连接关闭时被回滚,正是「提示启动成功但状态仍是未启动」「提示添加成功却找不到模块」的根因。已全部补上(并约定:with get_db() 内写完必须显式 conn.commit()),新增隔离回归测试 _mod_e2e.py(临时库副本,22 项断言全通过)。 |
| H5 体验 | 隐藏考勤 / 营位 / 自定义模块页的滚动滑块条(scrollbar-width:none + ::-webkit-scrollbar{display:none}),内容仍可正常滚动。 |
| 注意 | 改 .py 必须重启后台进程才生效(前端刷新即可);日常请双击 启动.pyw。 |
| 项目 | 说明 |
|---|---|
| 起因 | 用户新申请公云(PubYun)「内网域名」免费版,控制台内网映射为 huoshi.in.pubyun.net:13139 → 本机 127.0.0.1:9876;要求把域名写进后台,前端以后用这个域名联网访问。 |
| 已写入后台 | 「系统 → 服务器IP管理 → 联网 IP」= http://huoshi.in.pubyun.net:13139(POST /api/server-config/wan:后端自动补 http://、去尾斜杠)。「服务器访问地址」页据此实时生成 3 条联网地址:/attendance/、/campsite/、/guest/。⚠️ 只填裸地址(不带 /campsite 这类路径),否则清单里会拼成 /campsite/campsite/。 |
| 隧道当时 不通 | 域名解析正常:huoshi.in.pubyun.net 经 CNAME → v23.3322.net → 118.184.179.97(本机解析 0–2 ms、无 AAAA 记录);但 :13139 三次建连 3/3 全部「目标计算机积极拒绝」(WinError 10061),客户端进程亦无任何对外连接。 |
| 根因 ① 域名尾部 多一个空格 (已修复) | 公云客户端在 D:\tools\cargador\(cargador.exe 保活 + cargui.exe 界面),配置 cargador.jsn 存的是 "huoshi.in.pubyun.net "——域名尾部多了一个空格(从控制台复制域名时把「修改域名」前的空白一起选中了)。日志每 60 s 一条 keep run error: dial tcp: lookup huoshi.in.pubyun.net : no such host。→ 已备份并删掉空格( cargador.jsn.bak,70 → 69 字节);客户端 20:08:04 自行重启并读到新配置,no such host 消失。 |
| 根因 ② 内网域名密码 未通过 (待用户处理) | 修掉空格后立刻暴露第二层:开始连接 → 密码验证失败, 连接失败, retcode: Not Authenticated → client/Connect: retcode: 2,每 30–60 s 重复(确定性失败,不会自愈)。→ 需到公云控制台 内网域名 → 点「内网域名密码」按钮 复制那串密码(注意别带空格),在客户端界面里重新填一次并保存(GUI 保存会重写 cargador.jsn,只改文件容易被覆盖)。 |
| 顺手修 脚本误判 | _domcheck.py 原来把 DoH 答案里的 CNAME 当成 AAAA(查 AAAA 时解析器会一并返回 CNAME 链),于是对隧道域名误报「AAAA 与本机公用地址不一致」。已改为只保留与查询类型一致的记录,并把 CNAME 链显示在 A 行末尾;判定里对「A 经 CNAME 指向隧道/托管服务商」改为提示而非问题。 |
| 预期速度 | 公云「内网域名」免费版走公云云服务器中转(官网付费版标注 1 Mbps/映射),带宽与延迟由服务商决定。鸟瞰图底图 385 KB 首次仍要约 2–3 s,但已有一年强缓存(二次 0 字节);手机与电脑同 WiFi 时建议仍走局域网地址。 |
| 复验命令 | python _domcheck.py huoshi.in.pubyun.net 13139 —— 密码填对后应当看到 :13139 成功 3/3,前台地址即 http://huoshi.in.pubyun.net:13139/…。 |
| 本次未改动 | app.py / _serve.py / 各前端一行未改(前端本就支持填任意服务器地址,normServer() 会自动补 http://);只写了后台一条配置 + 修了客户端配置文件里的一个空格。 |
| 项目 | 说明 |
|---|---|
| 起因 | 用户先问「本机 IPv6 地址可以长期使用吗」,随后给出一个已注册域名 huoshi.run.place,要求实测联网速度。 |
| 结论 ① IPv6 地址 能不能长期用 | 同一 /64 前缀下本机同时存在 2~3 个全局地址:公用 …:c4a3:857:6876:2f83(后缀来源 Link,首选寿命 = 有效寿命)与临时地址(RFC 4941,后缀来源 Random,首选寿命仅约 3.4 h)。13:38 → 17:52 复测现场抓到轮换:…:710b:e0a9:c203:9fa0 首选寿命归 0(已废弃、仅剩有效寿命),由新地址 …:a1f9:afcc:7efa:37bf 接替上岗,两个临时地址一度并存;而公用地址纹丝不动(有效寿命期间还被 RA 刷新过一次),/64 前缀也未变。双采样(间隔 150 s)证明这些寿命只是纯倒计时(各减 150 s、地址不变),靠路由器 RA 周期性刷新维持。→ 能长期稳定的是「接口标识」,不能保证的是「/64 前缀」;临时地址严禁用于域名(几小时就换)。跨日对比: 9-24 与 9-25 记录的公用地址完全相同,实测至少 1 天+ 稳定。 |
| 结论 ② 域名体检 结果 | huoshi.run.place:解析正常且很快(本机 1–2 ms、公共 DoH 85–350 ms,TTL 3600),但 A 记录 = 119.98.236.104,恰好等于本机出口 IPv4(CGNAT 出口) → 80 / 443 / 9876 三个端口 3/3 全部超时,HTTP 与 HTTPS 均 10 s 超时 → 该域名当前对外完全不可达;且没有任何 AAAA 记录(IPv6 通道尚未配置)。NS = ns10~ns13.dnsexit.com;run.place 的 A 指向 64.182.209.151(属 DNSExit 自有服务器)→ 这是 DNSExit 的免费 DDNS 子域名,不是独立注册域;四台权威 NS 均在美国,TCP:53 建连 210–229 ms。 |
| 根因 | 走 CGNAT 时,DDNS 客户端「自动探测公网 IP」探到的是运营商 NAT 池的出口地址(不在自家光猫上),写进 A 记录后 → 解析完美、外部必然连不上。DNSExit 官方 FAQ 亦明示该现象(hostname 更新得很正常,但外部访客就是连不上)。 |
| 新增文件 | _domcheck.py:域名接入体检——一次跑完「记录(A/AAAA/CNAME/NS/TXT)+ 解析耗时 + 逐地址逐端口 TCP 建连 + HTTP 探测 + 与本机出口地址比对」,并自动识别「A 记录 = 自己出口 IPv4」这个 CGNAT 陷阱。用法 python _domcheck.py <域名> [端口 …]。 |
| 下一步 | ① 走 IPv6 直连:先用 _v6check.py 确认光猫放行,再补 AAAA 指向本机公用地址(DNSExit 的更新接口支持 ip= 参数上报 IPv6,更新间隔须大于 4 分钟);② 求稳:把 A 记录改成 frp 服务器的公网 IP;③ 现有 A 记录必须处理——留着会让双栈客户端先撞上一个永远超时的 IPv4 地址。 |
| 本次未改动 | 没有改 app.py / _serve.py 的绑定,也没有动域名上的任何记录(只读体检)。 |
| 项目 | 说明 |
|---|---|
| 起因 | 用户问「这台电脑现在有没有公网 IP / IPv6」,用来判断能否不用任何隧道、直接做端口映射。 |
| 结论 ① 公网 IPv4 | 没有(运营商级 NAT)。出口 IPv4 = 119.98.236.104(电信),但 tracert 第 2 跳是 100.70.0.1,落在 CGNAT 段(100.64.0.0/10)——光猫拿到的 WAN 地址是运营商内网地址,那个公网 IP 只是运营商 NAT 池的出口,不在你的路由器上。因此 IPv4 路由器端口映射完全无效,IPv4 场景只能走出站隧道(cpolar 与 frp 都是这个原理)。 |
| 结论 ② 公网 IPv6 | 有,且是真公网、无 NAT。网卡「以太网」全局地址 240e:36e:70f:8fd0:c4a3:857:6876:2f83(240e::/20 = 中国电信),另有临时地址(RFC 4941 隐私扩展,会定期轮换)。ipv6.icanhazip.com 回显的出口地址就是本机自己的地址,中间没有任何 NAT → 理论上可被公网直接访问。 |
| 要用起来 还差三道关 | ① 服务得监听 IPv6:_serve.py 与 app.py 现在绑 0.0.0.0(纯 IPv4)。⚠️ 实测 uvicorn 用 host="::" 在 Windows 上只是纯 IPv6,IPv4 客户端会被直接拒(WinError 10061)——正解是手工建双栈 socket 再交给 uvicorn:socket.create_server(("::",port), family=AF_INET6, dualstack_ipv6=True) + Server(cfg).run(sockets=[sock]),实测 netstat 出现 0.0.0.0:port 与 [::]:port 同一 PID,IPv4/IPv6 均通。⚠️ 副作用:双栈下 IPv4 客户端会被看成 ::ffff:192.168.1.2,app.py 里取 request.client.host 的三处需剥掉前缀,否则「在线用户」会显示成 ::ffff:…。② Windows 防火墙:已通——规则「火石营地管理 9876」= TCP 9876 入站允许(域/专用/公用全选),且当前生效的专用配置文件状态为关闭,Windows 侧不拦。 ③ 光猫/路由器的 IPv6 防火墙:未知(家用光猫通常默认拦入站,这是最大变数)。第三方外部节点回连验证未能得出结论(服务拒绝 IPv6 字面量、改用域名后对方节点又解析不了),改用手机蜂窝网实测才可靠。 ④ 地址会变: 240e:36e:70f:8fd0::/64 前缀是动态的(重拨即变),长期使用需要 DDNS。 |
| 新增文件 | _v6check.py:双栈监听一个临时端口,自动列出本机全局 IPv6 地址并打印出手机可直接打开的地址,用于判定光猫有没有放行 IPv6 入站(手机须用蜂窝流量、关掉 WiFi)。 |
| 建议 | 要零成本、最快→ 走 IPv6 直连(前提:光猫放行 + 接受地址会变);要稳定可靠→ 仍推荐 frp(CGNAT 下 IPv4 的唯一出路,且服务器对 IPv4/IPv6 客户端都友好、地址固定)。两者可并存。 |
| 本次未改动 | 没有改 app.py / _serve.py 的绑定(会带来 ::ffff: 副作用),等确认后再改。 |
| 项目 | 说明 |
|---|---|
| 问题现象 | 上一轮把二维码拆出去之后,登录后考勤与营位信息显示仍然偏慢,鸟瞰图尤其慢(要等好几秒)。 |
| 根本原因 (两条,都是「传输量」问题) | ① 服务器一个字节都没压缩:JSON 里全是重复字段名与中文,实测 /api/attendance 48.6 KB 原始、gzip 后只有 3.0 KB(16 倍);连页面本身都是裸传——后台首页 237.6 KB、H5 116.8 KB。② 鸟瞰图底图仍是 base64 塞在 JSON 里:底图是手机拍的照片(2275×1280 ≈ 376 KB),base64 后 502 KB,而且 JSON 响应永远无法被浏览器缓存——每次登录、每次切到鸟瞰图都白下 500 KB(实测限速 250 KB/s 下要 2.4 秒)。它占了整个营位模块传输量的 85%。 |
| 改法 ① 全站 gzip | 后端挂上 GZipMiddleware(minimum_size=1024)。JSON / HTML / JS 一律压缩;图片、视频、SSE 由 Starlette 默认排除清单自动跳过(不会给视频流做无谓压缩,也保证全景视频照常播放)。 |
| 改法 ② 底图独立端点 | 仿照二维码的做法:GET /api/camp-map?slim=1 只回元信息 has_map / updated_at / map_url(102 字节);图片本体走 GET /api/camp-map/image?v=<内容哈希>,二进制输出(376 KB,比 base64 少 25%),带 ETag + Cache-Control: public, max-age=31536000, immutable,支持 If-None-Match → 304。URL 里带内容哈希,换了底图 URL 就变,旧缓存自动失效,所以可以放心用一年强缓存。不带 slim 调老接口仍返回 image base64——没重新打包的旧 App / 旧小程序客户端不会因为这次改动看不到鸟瞰图。 |
| 改法 ③ 登录后预取 | 三端登录成功后延迟 800ms 顺手把底图预取进浏览器缓存(prefetchCampMap(),只用 new Image() 进缓存、不插 DOM)。延迟 800ms 是为了让登录、考勤 / 营位首屏那几个「gzip 后只有几 KB」的请求先发出去;底图虽然 376 KB 但只有一个请求,并行不会明显拖慢它们。等用户真正切到鸟瞰图时,底图往往已经下完了 → 秒开。 |
| 三端前端 | 后台 / H5:loadCampMap() 改取 ?slim=1 + map_url,campMapImage 从「base64 字符串」变成「图片地址」(赋值给 img.src 的用法完全不变,全屏观看共用同一份缓存)。uni-app:同样取 map_url,并按 App 平台要求拼成绝对地址(api.state.server + 相对路径)。游客页:同样改取 map_url(游客多在手机上走公网打开,收益最直接)。三端都保留 d.image 兜底,后端没重启也不会没有底图。 |
| 实测效果 (限速 150ms / 250 KB/s,模拟手机走 WiFi 或内网穿透) |
页面本身:后台首页 237.6 KB → 63.5 KB(3.7×);H5 116.8 KB → 35.1 KB(3.3×);游客页 28.7 KB → 9.7 KB(2.9×)。 考勤:48.9 KB / 697 ms → 3.3 KB / 295 ms。 鸟瞰图:切到鸟瞰图 2539 ms → 1882 ms(首次,底图 502 KB base64 → 376 KB 二进制);第二次起 6 ms(浏览器缓存,一个字节都不下载),改前则是每次都 2539 ms。 登录后总传输:590.6 KB → 5.4 KB(底图命中缓存时)。 |
| 验证 | ① 新增 _e2e/perf_login.js:用 CDP 给 Chrome 真实限速(150ms / 250 KB/s)跑完整登录流程,量页面 DOMContentLoaded、各接口传输字节与耗时、登录→考勤表格、进入营位→卡片、切鸟瞰图→底图可用(冷缓存与暖缓存各量一次)。② 新增 _e2e/run_mapslim.js(19 项全过):接口契约(slim < 2KB、无 image 字段、老接口仍带 image、图片是 image/*、ETag 与 v= 一致、immutable、If-None-Match 回 304)+ 后台 / H5 / 游客页三端 DOM(都走图片端点、底图真的画出来、营位图标有渲染、0 JS 异常)。③ 新增 _maptest.py(17 项全过,打临时副本库):改图 → 版本号变化 → 图片端点返回新图 → 换回原图版本号复原 → 清除 → has_map=false 且图片端点 404 → 恢复,并确认变更日志两条都在。④ 回归全绿: run_qrperf.js 24 项、run_camplist.js、run_video.js(h5 + admin)、run_admin_fs.js / run_admin_fs2.js、run_access.js、run_guest.js 44 项、run_pinratio.js 16 项、三端静态自检与 uni-app 真编译全部通过。 |
| 顺手修的两个「测试腐烂」 (与本次改动无关,但已长期变红) | ① run_guest.js 把入住 / 退房日期写死成 2026-09-21 ~ 2026-09-24 并断言 remaining_days==='3'——到了 2026-09-24 当天就变成 0,测试无端失败。已改为按「今天」现算。② run_pinratio.js 有两处过期:把 onPinScaleInput() 当成「全局缩放」用,而它早已改成单个营位的图标大小;手机端又忘了置 loggedIn=true 导致 loadUiConfig() 不执行。已修好,并且比例常量改成从 /api/ui-config 现读,不再写死 0.032。(该脚本 7 项红 → 全绿,仍在新旧代码上都验证过「三端图标比例一致」。) |
| ⚠️ 需要重启后端 | 同前:改 .py 必须重启进程才生效。前端已做兼容兜底(d.image 兜底),不重启也不会坏,只是享受不到 gzip 与缓存提速。 |
| 项目 | 说明 |
|---|---|
| 问题现象 | 登录后考勤数据几乎立刻出来,营位信息却要等很久。 |
| 根本原因 | 营位接口 GET /api/campsites 把每个营位的电表二维码照片(手机拍的 1888×2304、未压缩 PNG,单张 1.16 MB)以 base64 塞进 JSON 一起返回。实测 3 个营位 = 3137 KB,而考勤接口只有 48 KB,相差 65 倍。营位越多越慢(30 个营位就是 30 MB+),手机走 WiFi / 公网隧道会长时间白屏。而这类图片只有「卡片上一个小缩略图」和「点开看大图」两种用途,完全没必要跟着列表每次全量传。 |
| 后端改法 | ① 列表与详情接口不再返回整段 base64,改为 has_qr(有没有)+ qr_url(原图地址)+ qr_thumb_url(缩略图地址);② 新增独立图片端点 GET /api/campsites/{id}/qr,加 ?thumb=1 由服务端用 Pillow 缩到长边 260px、JPEG 输出(18 KB,比原图小 65.8 倍);③ 图片响应带 ETag + Cache-Control: max-age=604800,内容没变时回 304——浏览器与手机只真正下载一次;④ 详情接口保留 ?qr=1 可要回完整 base64(兼容旧调用 / 备份)。 |
| 数据安全网 (最重要的一条) | 原保存接口写的是 body.get("qr_code", "")——前端没传就会被写成空串,把用户上传的电表照片冲掉。现在改成:请求里没有 qr_code 键 = 前端没动它 → 保留数据库原值;只有显式传了(重新选图 / 点「清除」)才覆盖。三端前端也同步改成「只有用户真的动过二维码才提交」,双保险。 |
| 三端前端 | 卡片 / 列表 / 详情用缩略图端点(带 loading="lazy",滚到才加载);全屏查看与编辑回显用原图端点(点开才下,之后走 HTTP 缓存,画质不变);未改动二维码时不提交 qr_code。 |
| 实测效果 | 营位列表 3137.1 KB → 1.9 KB(约 1600 倍,与考勤 48.6 KB 已是同一量级);详情 1588 KB → 1.9 KB;缩略图 18.1 KB;原图仍 1190 KB,但只在点开时下载且有缓存。营位数量越多,收益越大。 |
| 验证 | 新增 _e2e/run_qrperf.js(真 Chrome,页面内实测各接口字节数 + 真鼠标点开二维码,共 24 项全过):列表体积、缩略图 / 原图端点、卡片 img 真的加载出图(naturalWidth>0,不是 404 破图)、全屏用的是原图(1888px 明显大于缩略图 213px)、编辑回显可见、未改动时 campQrTouched=false、无 JS 报错。另有副本库专项测试确认「不带 qr_code 保存 → 原值不变」「带新图 → 更新」「带空串 → 清空」「状态接口不碰二维码」四种情况全部正确。三端静态自检与 uni-app 真编译均通过。 |
| ⚠️ 需要重启后端 | 后端是 FastAPI 单进程,改 .py 必须重启进程才生效(restart_server 只做换端口的优雅重启,不会重新加载代码)。前端已做兼容兜底:后端未重启时仍读旧的 qr_code,功能不会退化,只是享受不到提速;重启后自动走新路径。 |
| 项目 | 说明 |
|---|---|
| 营位 · 列表模式 (新功能) | 营位列表页在原有「卡片 / 鸟瞰图」之外新增第三种显示模式:列表。三端(后台 / H5 / uni-app)统一为 🗂️ 卡片 · 📋 列表 · 🗺️ 鸟瞰图,切换按钮横排放在搜索框下方,当前模式高亮选中。列表模式一行一个营位,信息密度明显高于卡片,适合快速浏览与核对;字段顺序与卡片、后台表格保持一致:编号 · 姓名 · 状态 · 联系电话 · 入住日期 · 退房日期 · 剩余天数 · 入住类型 · 消费金额 · 备注。 |
| 各端列表形态 | 后台:真表格(第一行是表头,表头吸顶),末列是操作按钮 📷 全景图 / ✏️ 编辑 / 🗑️ 删除,点整行任意位置打开营位详情。H5 / uni-app:行式卡片,首行是「编号 + 姓名 + 状态徽标」,下面一行是电话 / 入住类型 / 入住~退房 / 剩余天数 / 金额 / 备注(备注为空时该段自动不显示),右下角是同样的三个操作按钮;点整行打开营位详情。 |
| 剩余天数配色 | 列表里剩余天数会变色提醒:已过期显示为灰、2 天内到期显示为红。只对「已入住 / 已预订」生效——空闲 / 维护营位的 remaining_days 只是占位 0,不该用红字「今天到期」吓人。卡片与鸟瞰图不变。 |
| 联动与权限 (其他一切不变) | 列表与搜索、状态统计卡筛选、实时同步(WS)完全联动:搜索关键词或点状态卡后,列表行数与其他视图同步收敛;后台 / H5 收到同步消息重新拉数据后,列表也一并重画。列表不是只读视图:可编辑、可删除、可看全景图(受 canWrite('campsite') 权限控制),后台右下角「+ 新增营位」在列表模式同样可用(卡片 / 列表直接弹录入表单,鸟瞰图仍是「先选状态再落位」的老流程)。卡片模式与鸟瞰图模式的行为、布局、全屏观看等一律未改动。 |
| 验证 | 新增端到端脚本 _e2e/run_camplist.js(真实 Chrome + 真实鼠标点击切换按钮,只读不改库):后台 20 项、H5 12 项全部通过(三按钮存在、切换后互斥显隐正确、高亮正确、表格行数/行项数 = 当前筛选结果数、点行能打开详情、搜索与状态筛选联动、切回卡片 / 鸟瞰图正常、无 JS 报错)。uni-app 侧用 _e2e/sfc_check.js 做模板 + <script setup> 真编译通过,并逐项核对了模板里引用的 20 个函数在脚本中均有定义。 |
| 项目 | 说明 |
|---|---|
| 产物 | 火石营地uniAPP/火石营地.apk(14.39 MB,2026-09-24 14:42 生成)。上一版(今天 13:49)已备份为 火石营地uniAPP/火石营地_旧.apk,需要回退随时可用。 |
| 本次带上了什么 | 把登录页用户ID 示例提示由 FS- 改为 HS- 的改动打进了安装包。包内 assets/apps/__UNI__BD258C8/www/app-service.js 实测:HS- 1 处、FS- 0 处、如 HS-XXXXXXXX 1 处。(上一版 13:49 的包里登录页仍是旧的 FS- 提示。) |
| 应用标识 (与旧包逐项核对一致) | 包名 uni.app.huoshi、appid __UNI__BD258C8、versionName 1.0.0、versionCode 100、minSdk 21、targetSdk 28、debuggable=false(正式包)。版本号按要求保持不变,未改动 manifest 里任何版本字段。 |
| 可直接覆盖安装 | 新包签名 SHA1 77:94:7B:70:6F:1F:43:90:F8:22:4F:F1:C6:00:F0:A1:B4:E6:58:91 与旧包完全一致(同一份 D:\qcd.keystore 签出)→ 手机上已装旧版的设备直接安装即可升级,不需要先卸载,本机数据不丢。 |
| 打包方式与排队 | HBuilderX 命令行云打包(cli pack --platform android),证书类型 Android 自有证书(D:\qcd.keystore,别名 qcd)——不是自定义调试基座。(keystore 密码不记录在项目文件里,需要时另行提供。)提交时队列第 119 位,全程约 8 分钟(14:33 提交 → 14:42 出下载地址)。 |
| 验证结果 | 下载后四项校验全部通过:① 是合法 APK(795 个条目);② 签名与旧包一致;③ manifest 真解析出的包名 / versionCode / versionName / debuggable 全部符合预期;④ 包内应用脚本确认含本轮改动(如 HS-XXXXXXXX / 修改登录密码 / 自动登录 / applyNewPassword / clearAutoLogin / my-password,按中文文案与接口名双重复核,规避 release 压缩改变量名导致的误判)。 |
| 说明 | 本轮只重新编译打包,未改动任何源码;后端、后台、H5 均不受影响。打包过程中 HBuilderX 提示「应用缺少隐私政策配置」,仅影响上架国内应用市场,自用安装无需处理。 |
| 项目 | 说明 |
|---|---|
| 产物 | 火石营地uniAPP/火石营地.apk(14.39 MB,2026-09-24 13:49 生成)。打包前把上一版自动备份为 火石营地uniAPP/火石营地_旧.apk(09-21 那版,保留不覆盖,需要回退随时可用)。 |
| 打包方式 | HBuilderX 命令行云打包(cli pack --platform android),证书类型 Android 自有证书(D:\qcd.keystore,别名 qcd)——不是自定义调试基座。(keystore 密码不记录在项目文件里,需要时另行提供。) |
| 应用标识 (与旧包逐项核对一致) | 包名 uni.app.huoshi、appid __UNI__BD258C8、versionName 1.0.0、versionCode 100、minSdk 21、targetSdk 28、debuggable=false(正式包)。版本号按要求保持不变,未改动 manifest 里任何版本字段。 |
| 可直接覆盖安装 | 新包签名 SHA1 77:94:7B:70:6F:1F:43:90:F8:22:4F:F1:C6:00:F0:A1:B4:E6:58:91 与旧包完全一致(同一份 keystore 签出)→ 手机上已装旧版的设备直接安装即可升级,不需要先卸载,本机数据不丢。 |
| 这一版带上了什么 | 对 release 包内的 assets/apps/__UNI__BD258C8/www/app-service.js 实测比对(旧包 → 新包):「修改登录密码」0→1、「自动登录」0→1、my-password 0→1、applyNewPassword 0→1、clearAutoLogin 0→1。即「🔑 修改密码」「自动登录」勾选框、以及关闭全景视频报错的修复都随这个包装进手机了;09-21 那版旧包这三项都没有。 |
| 云端排队 | 提交时队列第 136 位,约 7 分钟完成全流程(本地编译 → 云端排队 → 打包 → 出下载地址)。 |
| 验证结果 | 下载后四项校验全部通过:① 是合法 APK(795 个条目);② 签名与旧包一致;③ manifest 真解析出的包名 / versionCode / versionName / debuggable 全部符合预期;④ 包内应用脚本确认含本轮全部改动(按中文文案与接口名双重复核,规避 release 压缩改变量名导致的误判)。 |
| 说明 | 本轮只重新打包,未改动任何源码;后端、后台、H5 均不受影响。打包过程中 HBuilderX 提示「应用缺少隐私政策配置」,仅影响上架国内应用市场,自用安装无需处理。 |
| 模块 | 变更 |
|---|---|
| 新增功能 | H5 移动端与 uni-app App 的顶栏都多了一个 🔑 图标,点开是「🔑 修改登录密码」对话框:填原密码 / 新密码 / 确认新密码,确认后直接写进系统——后台「用户ID」里看到的就是新密码(不是只改本机,改的是系统里这个账号的登录密码)。 |
| 为什么要新开一个接口 | 后端原来那个 change-password 是给管理员重置别人密码用的,它受「用户ID管理」权限保护 —— 普通账号(如只给考勤可编辑的账号)调用会被 403 拦下,自己的密码都改不了。所以新增一个自助改密接口 POST /api/user-ids/my-password:必须带原密码,后端校验通过才写库;因为「改自己的密码」本来就要求知道原密码,所以它不受模块权限限制(否则普通账号永远改不了自己的密码)。原来的管理员重置接口保持不变,仍然只有管理员能用,两个口子互不影响、权限也没被放宽。 |
| 校验规则 | 前端先拦一层(原密码为空 / 新密码不足 4 位 / 两次输入不一致 / 新旧密码相同),后端再拦一层(原密码错误 / 新密码不足 4 位 / 与原密码相同 / 用户ID 无效),错误原因都是中文提示、直接弹在界面上。 原密码错时弹窗不会关,可以就地重填再试。账号从未设过密码(库里为空)时按「首次设置」处理,直接写入。 |
| 改完本机同步 | 改密成功后,本机保存的密码同步换成新密码(自动登录用的那份、以及断线重连重新鉴权用的那份)。这样自动登录照旧可用,不会出现「刚改完密码,下次打开却因本机还是旧密码而登不进去」。没开自动登录的账号依然不留密码(与「退出登录」的约定一致)。 |
| 后台同步 | 写库即时生效:后台「用户ID」列表里的密码即刻变成新密码(同一张 user_ids 表,无需任何手工操作);同时写入变更日志(模块=用户ID / 操作=修改密码 / 操作人=该账号 / 明细=原密码 → 新密码),并向三端推送一次同步消息,正在打开的后台用户列表自动刷新。 |
| 验证 (真实 Chrome + 接口层,隔离库) | 新增 _pwtest.py + _e2e/run_password.js:整库复制到临时副本库、临时端口 8794 起服务,测试账号只建在副本里,跑完删副本。17 项接口层断言 + 24 项真 Chrome 端到端断言,全部通过:· 接口层:普通账号(非管理员)改自己密码不被 403 拦下;原密码错 / 新密码太短 / 与原密码相同 / 用户ID 不存在 → 都被拒绝;改完新密码能登录、旧密码失效;后台 /api/user-ids 列表里的密码已是新密码;变更日志记到了操作人与「原值→新值」;普通账号调用管理员重置接口仍被 403 拦下(权限没被放宽)、管理员调用照常可用。· 真 Chrome:顶栏 🔑 入口可点 → 弹窗显示且用户ID 回显、原密码自动带出 → 四种非法输入各自弹出正确中文提示 → 原密码错误被后端拒绝且弹窗不关、没被误报成「网络故障」 → 正确提交后弹窗自动关闭 + 提示「密码已修改」→ 本机凭据换成新密码 → 刷新页面仍能自动登录直接进系统 → 再用新密码改回原密码也正常(连续改密),全程 0 个未捕获异常、0 次 alert。 · 真实库零污染:副本里的测试账号与改密日志,在真实库里一条都没有。 |
| uni-app 真机验证 (模拟器 emulator-5560) | 本轮把 uni 端也放到真实设备上走了一遍完整流程(设备连的是副本库的临时服务,测试账号 ZZPWD9,真实库零污染):· 顶栏 🔑 图标可点,弹出「🔑 修改登录密码」,用户ID 回显当前账号、原密码自动带出; · 两次新密码填得不一样 → 弹出「两次输入的新密码不一致」,弹窗不关; · 原密码填错 → 弹出系统提示「修改失败 / 原密码不正确」(由后端拒绝,前端如实展示中文原因); · 原密码正确 → 提示「密码已修改,后台已同步」并自动关闭弹窗;随后在副本库里查到密码已变为新密码,变更日志写着「真机改密测试(ZZPWD9)在本机修改登录密码:原密码 666888 → 新密码 newpass123」; · 杀掉 App 重新打开 → 直接自动登录进系统(证明本机保存的密码确实同步成了新密码); · 全程 logcat 无任何 JS 异常。 |
| 改动文件 | 后端 app.py(新增自助改密接口 /api/user-ids/my-password + 权限守卫豁免 + 两个改密接口都补上变更日志)、火石营地移动端/index.html(顶栏 🔑 + 改密弹窗 + doChangePwd())、火石营地uniAPP/common/api.js(changePassword() / applyNewPassword())、火石营地uniAPP/pages/index/index.vue(顶栏 🔑 + 改密弹窗 + 返回键优先关弹窗)。后台管理端(电脑)未改动(它本来就有管理员重置用户密码的入口)。 |
| 模块 | 变更 |
|---|---|
| 新增功能 | 手机端(H5 移动端 与 uni-app App)的登录页都加了「自动登录(下次打开直接进入系统)」勾选框。勾上之后再打开页面 / App,不用再点「连接 / 登录」按钮:系统会自动拿本机保存的用户ID + 密码登录,直接进入系统。 |
| H5 端 (火石营地移动端) | 原来那个「保存密码(下次自动登录)」其实只是把密码存在浏览器里,打开时并不会真的登录,文案容易让人误解。现在改成明确的「自动登录」勾选框(#autoLoginChk):勾选后把 fs_auto_login=1 与密码存在本机,页面一打开就静默调一次登录接口,成功即直接进系统(进入后弹出的仍然是「请选择要操作的功能」对话框,与手动登录完全一致)。 |
| uni-app 端 | 登录页同样把「保存密码(下次自动登录)」换成「自动登录」勾选框(pages/login/login.vue),勾选状态随配置一起落盘(camp_uni_config 新增 autoLogin 字段)。App 启动进入登录页时,只要开关为开且本机有凭据,就自动登录并直接跳进首页(uni.reLaunch 到 pages/index/index),登录页只一闪而过。 |
| 退出登录 | 主动点「退出登录」=明确要求离开,所以会清掉本机保存的密码并关闭自动登录开关(否则下次打开又自己进去,「退出登录」就失去意义)。退出后重新打开会停在登录页,需要重新输入密码。用户ID / 服务器地址仍然保留,下次少输一点。 |
| 凭据失效时 | 如果本机存的密码已在后台被改过(或账号被删),自动登录会失败。失败时不弹生硬报错、也不会卡在空白页:停在登录页并提示「自动登录失败:登录密码错误,请重新登录」,同时自动关掉自动登录开关,避免每次打开都白试一次、反复报错。若只是网络/地址不通(不代表密码错),开关会保留,下次打开还会再试。 |
| 兼容旧版本 | 以前勾过「保存密码」的用户(本机已存密码、但还没有新的自动登录标记)自动视为已勾选自动登录,升级后不用重新设置——体验上「还是像以前一样打开就进去」。H5 看 fs_pwd 是否存在,uni-app 看存档里 autoLogin 字段是否缺失。 |
| 验证 (真实 Chrome 端到端) | 新增 _autologintest.py + _e2e/run_autologin.js:整库复制到临时副本库、临时端口起服务,在副本里建测试账号 ZZAUTO1,用真鼠标事件走完整流程(点复选框 → 填表 → 点「连接」→ 刷新页面 → 点退出 → 再登录),23 项全过:勾选后重开确实直接进系统(全程未点任何按钮)、不勾选则本机不留密码、退出后不再自动登录、密码错时停在登录页且有中文提示。测试全程真实库零污染(真实库里既没有该测试账号、也没有一条属于它的登录日志)。uni-app 端另用 HBuilderX 自带的 @vue/compiler-sfc 做了真实 SFC 编译校验(新增 _e2e/sfc_check.js:模板与 <script setup> 一起真编译,比只做语法检查更严格):App.vue / pages/login/login.vue / pages/index/index.vue 全部编译通过。 |
| 改动文件 | 火石营地移动端/index.html(登录卡片 + doConnect 支持静默模式 + 新增 tryAutoLogin() + 退出 / 凭据失效时清理)、火石营地uniAPP/pages/login/login.vue、火石营地uniAPP/common/api.js(state.autoLogin + clearAutoLogin())、火石营地uniAPP/pages/index/index.vue(退出登录时清凭据)。后台管理端(电脑)是管理员自用、不需要自动登录,未改动;后端未改动。 |
getBoundingClientRect 已修复 · 真机验证)| 模块 | 变更 |
|---|---|
| 问题现象 | uni-app 端全景视频能正常播放,但一关闭就在控制台蹦红字:Uncaught TypeError: Cannot read properties of null (reading 'getBoundingClientRect') at uni-app-view.umd.js:7。不影响播放功能,但每次关闭都报,看着像坏了。 |
| 根因 | 全屏播放器原来用 v-if="showVideo",关闭时 Vue 把整块节点(连同原生 video 组件)立刻卸载;而 App 端视图层(uni-app-view.umd.js)在同一拍仍要对这个刚被移除的节点做 getBoundingClientRect 布局量测 → 拿到的元素已是 null → 抛异常。与视频编码、地址、网络都无关,纯粹是「原生组件被销毁」的时序问题。 |
| 修复 | 三层一起改,缺一不可: ① 模板改 v-show(节点常驻、只切显隐)—— 元素永远在 DOM 里,量测拿得到,从根上消除空指针;② closeVideoPlayer() 改成「先暂停 → 只置 showVideo=false → src 延迟 600ms 再清空」(清 src 是为释放解码器,但必须等视图层量测完;期间若又打开视频,定时器会被取消,不会误清);③ 打开时 nextTick 里用 uni.createVideoContext('campVideo') 显式 play();同一个视频再点一次走 seek(0)+play() 从头播。 |
| 顺带修掉 | 手机返回键原来没管视频层:按返回键会先关掉底下的「营位详情」对话框,而盖在最上面的全屏视频还挂在屏幕上关不掉。onBackPress 里把视频层提到最高优先级(先关视频 → 再关详情 → 再关全屏/各处弹窗)。onUnload 退出页面时也顺手暂停视频并清掉延迟定时器。 |
| 真机验证 (模拟器 emulator-5560) | 在 HBuilderX 真机调试环境里按真实操作走了一遍,用设备日志(logcat)判定,不靠肉眼: · 修复前: 22:24:45 / 22:24:57 两条 getBoundingClientRect 报错(与用户截图时间完全对上);· 修复后(热更新到设备,设备上的 app-service.js 已确认含 v-show + campVideo):打开 → 关闭 → 再打开 → 关闭 → 再打开 → 按返回键,连续多轮,logcat 里 0 条 Uncaught 报错;· 关闭后界面干净回到详情对话框,没有残留的黑块/幽灵视频(这条专门验证了 v-show 不会让原生视频层留在屏幕上);· 返回键:第一次返回只关视频(详情对话框保留)→ 第二次返回才关对话框,行为正确。 |
| 改动文件 | 仅 火石营地uniAPP/pages/index/index.vue(模板 1 处 + closeVideoPlayer/playCampVideo/onBackPress/onUnload)。后台与 H5 不受影响(它们用的是 Web 原生 video,没有这个时序问题)。 |
| 模块 | 变更 |
|---|---|
| 系统 · 服务器访问地址 | 「系统 → 服务器访问地址」列表里补上游客登记地址,与其他地址一样由「服务器IP管理」的端口 / 局域网 IP / 联网 IP 实时生成、点一下即可复制,无需手工维护。新增两条: · 游客端 · 局域网 · 入住登记 → http://<局域网IP>:<端口>/guest/(手机与电脑同一 WiFi 时用)· 游客端 · 联网 · 入住登记 → <联网地址>/guest/(手机用流量、或游客扫现场二维码时用)列表由 5 条变 7 条(已配联网 IP 时);若还没配联网 IP,则由 4 条变 5 条。序号由程序自动生成,始终 1..N 连续。 |
| 改动位置 | 仅后端 app.py 的 _access_rows()(生成地址清单的函数)增补两条;后台前端未改动——列表本来就是按接口返回逐行渲染的,新条目自动出现。 |
| 自检工具 | _guesttest.py 增加第 10 组断言(地址清单含 2 条 /guest/、以 /guest/ 结尾、局域网地址端口与后台端口一致、序号连续无空号),53 项全过;_check_ui.py 与 _smoke.py 的静态挂载清单补入 /guest/;新增 _e2e/run_access.js(真实 Chrome:从侧栏点进「服务器访问地址」→ 表格渲染 → 行数 / 序号 / 名称 / 地址与接口逐条比对 → 游客两行可点击复制 → 序号连续 → 0 异常,17 项全过)。顺带修正 _check_ui.py 里几个已失效的旧标记(角色输入框 / 导入弹窗已改名或移除,导致常年误报 False),现全部为 True。 |
| 模块 | 变更 |
|---|---|
| 游客入住登记 (新增页面) | 新增游客自助入住登记页,随程序一起放在后端目录下的独立文件夹 游客入住信息自助录入/(页面 index.html),访问地址 /guest(/guest/ 亦可)。游客无需账号密码,用手机打开就能自己登记。入口:后台 9876 端口 + /guest;也可把该地址/二维码贴在营地现场。 |
| 选营位 (鸟瞰图 + 下拉) | 鸟瞰图上只显示「空闲」状态的营位(已入住 / 已预订的营位完全不出现,连编号与客人姓名都不会显示,不泄漏在住客人信息)。点图标即选中,选中的图标会高亮并显示「已选择营位:xxx」。同时提供「营位编号」下拉列表——选项全部由系统下发,只能选、不能编辑,与鸟瞰图双向同步(点图标会同步下拉、选下拉会高亮图标)。若某个空闲营位还没在鸟瞰图上定位,会在图下方以编号小标签列出,仍可选。底图可放大(1× / 1.5× / 2.2× / 3×)便于挑选。 |
| 录入项 | 游客自己填写 5 项:① 营位编号(仅选择)② 姓名 ③ 联系电话 ④ 入住日期(日期控件)⑤ 退房日期(日期控件);另加入住类型(常驻 / 临时)由游客自行选择,共 6 项。提交前弹出确认框复核「营位编号 / 姓名 / 联系电话 / 入住类型 / 入住日期 / 退房日期」,确认后才写入。 |
| 提交落库 (直连后台) | 提交后信息直接写进该营位在 campsites 表里的记录(替换该营位原有数据):姓名 / 联系电话 / 入住日期 / 退房日期 / 入住类型 覆盖写入,状态置为「已入住」,剩余天数按退房日期自动换算;同时在 campsite_stays(入住记录)里追加一条,备注标注「游客自助登记入住」。消费金额 / 备注保持该营位原值不动(由营地工作人员后续补充)。 |
| 校验与防错 | 前端 + 服务端双重校验:营位必选、姓名必填、电话须 6~20 位数字、两个日期必填且退房日期不得早于入住日期、入住类型必选。服务端在写入前再校验一次「该营位必须仍是空闲」,避免两位游客同时登记到同一营位;2 秒内重复提交会被节流,防手机连点重复登记(校验失败不计入节流,改错后能立刻重提)。 |
| 后台 · 登录信息 (游客登录) | 游客一打开登记页,后台就记一条「游客登录」到 access_logs(同一 IP 30 分钟内只记一条,避免刷屏)。后台首页「🕘 历史记录」右侧「登录 / 退出」筛选里能看到「登录 · 游客」,与员工账号登录记录并列。操作人身份由后端强制写成「游客」,游客页面无法冒充某个账号。 |
| 后台 · 历史记录 (改了哪几条 + 原值对比) | 每次登记往 change_logs 记一条 action=guest_checkin(标签显示「游客入住登记」)、operator=游客、operator_id=guest。摘要写明哪个营位、姓名、入住类型,以及逐项「原值 → 改后值」;detail.before / detail.after 也会在历史条目里渲染成「原值」与「改后值」两栏(营位编号 / 姓名 / 手机号 / 状态 / 入住类型 / 入住日期 / 退房日期),与以前的数据直接对比。 |
| 实时同步 | 游客登记成功会广播营位变更(sync entity=campsite),后台 / H5 / uni-app 三端立刻刷新;被登记的营位随即从游客页的「可选中」列表里消失。 |
| 新增接口 | POST /api/guest/enter(进入页面:记「游客登录」+ 返回后端地址)、POST /api/guest/checkin(提交登记)。专供游客页使用,不需要账号、不受写守卫限制(游客本来就没有账号)。改动仅新增,未触碰既有接口行为。 |
| 开发 · 自检工具 | 新增 _guesttest.py(隔离验收:整库复制到临时副本 + 临时端口,覆盖建表 / 游客登录留痕 / 9 种表单校验 / 正常落库 / 重复登记被拒 / 防连点节流 / 变更日志 / 历史展示,49 项全过);新增 _e2e/run_guest.js(真实 Chrome 端到端:页面与「游客登录」标识 / 鸟瞰图只画空闲营位 / 下拉由系统下发 / 图标尺寸严格等于「底图宽 × pin_ratio × pin_scale%」且可点区 ≥46px / 真鼠标点图标与用下拉两条选中路径 / 缩放后比例仍一致 / 确认弹窗与提交成功 / 登记后营位从可选中消失 / 库里数据与后台历史,44 项全过)。两个脚本都在临时库副本上跑,真实数据零污染。 |
| 模块 | 变更 |
|---|---|
| 全景图视频 (根因) | 手机端 H5 点开视频一直转圈、放不出来,而后台(桌面浏览器)与 uni-app 正常。根因不在前端代码,在视频文件本身:相机 / 手机导出的 MP4 常把 moov(索引元数据)放在文件末尾(布局 [mdat 45MB][moov])。播放器必须先读到 moov 才能解码,于是得先去取文件尾部;桌面 Chrome 支持 HTTP Range、能先取尾部再播,手机浏览器 / 微信等内嵌 WebView / 部分 App 原生播放器对「尾部取数」支持很差 → 卡在缓冲、永不开始。实测该文件 moov 位于第 45,273,589 字节(文件末尾),已确认。 |
| 后端 · MP4 无损重排 (新增) | 新增 mp4_make_faststart():把 moov 搬到文件最前([moov][mdat]),只做字节搬移、不重新编码,秒级完成、画质无损(同时修正 moov 内 stco/co64 的 chunk 偏移)。上传即自动处理(POST /api/campsites/{id}/media 保存 mp4/mov 后自动重排),今后上传的视频不会再出现这个问题。另新增 POST /api/media/faststart 一键修复 media 目录里所有历史遗留视频。分片(fragmented)MP4 自动跳过以免改坏。 |
| H5 · 播放取址 (改进) | 全景图取址改为相对后端基准地址解析(与接口同一基准),不再用页面自身的 location.origin——H5 页面与后端不同域名时,/media 会指错主机。同源时行为不变。 |
| H5 · 播放状态提示 (新增) | 全屏播放器增加状态反馈:缓冲中显示「缓冲中…」;真的放不了时显示具体原因(加载被中止 / 网络错误 / 解码失败(该编码手机可能不支持)/ 地址无效),并给出兜底入口——点提示用系统播放器在新窗口打开,不再让人盯着黑屏干等。 |
| 开发 · 自检工具 | 新增 _e2e/check_mp4.js:用 Range 取文件头 2KB 断言 moov 在 mdat 之前(faststart),并用真实 Chrome 断言仍可解码(readyState≥1 / 时长 / 分辨率)——防「搬移后偏移写坏」。已用「人为造一个非 faststart 副本」做过反向验证:该副本会被判 FAIL(moovAt=-1)却仍能解码,正好复现「桌面能播、手机放不了」的现象。 |
| 模块 | 变更 |
|---|---|
| 营位 · 全景图视频 (修复) | 修复「全景图里的视频没有播放按钮、点不动」:以前画廊把视频内联成一个 <video controls> 塞进 1:1 的小方格里,控件条被卡片裁掉、桌面端还要把鼠标移上去才显形,看上去就是一块黑方块。现在改为 封面 + 中央 ▶ 播放按钮,点一下弹出全屏播放器(原生控件 + 自动播放,右上角 ✕ 关闭、Esc 可关),关掉即暂停并释放视频地址。后台与 H5 两端一致。 |
| uni-app · 全景图视频 (修复) | 修复「卡片里只有一块黑、真视频却跑到对话框下面还能播」:App 端 <video> 是原生组件,被塞进「弹窗 + scroll-view」里时位置由原生层另算,于是出现黑块留在卡片、视频自己漂到别处。现在 uni 端详情对话框里不再放 video 组件,与后台 / H5 一样只显示封面 + ▶,点击后由放在页面最外层的全屏播放器播放(原生组件放在最外层才不会错位)。加载失败会提示「请检查网络或换个格式(建议 H.264 MP4)」。 |
| 三端 · 全景图取址 (修复) | 修复「手机上视频 / 照片不显示」:媒体接口返回的是 /media/... 相对路径,uni-app 编译版会把它当设备本地路径去找文件。现在三端统一拼成完整网络地址(uni 用服务器地址、H5 用当前域名)后再交给播放器。 |
| 回收站 · 全景图 (新增) | 删除营位时,它的全景图(照片 / 视频)一并进「删除的数据」:回收站列表显示 📷 数量,详情里可直接预览图片 / 播放视频,恢复营位时全景图会重新挂回该营位;只有「彻底删除 / 清空回收站」才真正删磁盘文件。 |
| uni-app · 营位卡片 | 卡片重排为商业化布局:左侧数据 8 行(字段与「修改营位」对话框完全一致)、右上状态徽标 → 其下电表二维码(放大)→ 再下📷 / ✏️ / 🗑️ 三个按钮一行;四字标签不换行,状态与二维码居中对齐。 |
| H5 · 营位卡片 | 左侧数据对齐修正(图标独立固定槽 + 标签定宽,值统一左对齐),标签改为与后台一致的入住日期 / 退房日期 / 剩余天数(剩余天数口径统一:剩余 N 天 / 今天到期 / 已过期 N 天)。 |
| 开发 · 自检工具 | 新增 _e2e/run_video.js:真实 Chrome + 真鼠标事件验证「封面 ▶ 可见 → 点击弹出全屏播放器 → 视频真的解出时长与分辨率(能播的硬证据)→ 关闭后暂停并释放地址」,后台与 H5 各跑一遍。 |
| 模块 | 变更 |
|---|---|
| 用户ID · 前缀 | 生成的用户ID 前缀由 FS- 改为 HS-(generate_user_id())。新生成的用户ID 形如 HS-XXXXXXXX。(已在库里、用 FS- 登录的旧账号不被改名——改名会使原登录失效;如确需把存量账号也改成 HS- 前缀,请单独提出,我会做一次性数据迁移。)(补充)前端提示文案已统一:H5 与 uni-app 登录页「用户ID」输入框的示例提示,原为 FS-XXXXXX / FS-XXXXXXXX,现已同步为 HS-XXXXXX / HS-XXXXXXXX;顺手把 4 个历史验证脚本里的测试账号夹具(_verify2/5/6/7.py 中的 FS-TEST*)一并改名。(收尾)该提示改动已随 2026-09-24 14:42 那次重新云打包进入安卓安装包(见「本轮更新」)。至此全项目再无 FS- 用户ID前缀残留(本行历史记录中的 FS- 属变更留痕,保留原样;界面上「全屏」相关的 FS_PIN_W / fs-* 类名与 fs_* 本地存储键与用户ID无关,未改动)。 |
| 营位 · 全景图 (新增) | 营位详情新增「📷 全景图」:即去营位实际位置拍摄的照片 / 视频。后端新增 campsite_media 表(id / site_id / file / mtype / created_at)与 media/campsite 存储目录(挂静态目录 /media 供三端读取);提供三个接口:GET /api/campsites/{id}/media(列表)、POST /api/campsites/{id}/media(上传,multipart file,支持图片与视频)、DELETE /api/campsites/{id}/media/{mid}(删除,同时删磁盘文件)。营位详情接口 GET /api/campsites/{id} 一并返回 media 列表;删除 / 清空营位会一并清理其全景图。上传 / 删除走既有「营位管理」写守卫与实时同步广播。 |
| 后台 · 全景图 | 营位详情弹窗「📜 入住记录 / 🕓 变更记录 / 操作日志」旁新增「📷 全景图」页签:画廊展示图片(点开看大图灯箱)与视频(内联播放),可多选上传、每条单独删除。后台为管理员恒可编辑。 |
| H5 / uni-app · 全景图 | 手机端营位详情同样可查看全景图;有「营位管理」写权限的账号可在 H5 / uni-app 上传(拍照或相册,图片 / 视频)与删除。三端改动经实时同步,任一处上传 / 删除,其它端打开该营位详情即看到最新。 |
| 模块 | 变更 |
|---|---|
| 后台 · 月度考勤实时显示 (修复) | H5 / uni-app 新增月度考勤后,后台没实时显示。根因:后台 WS 处理 handleSync 缺少 monthly 分支——sync 广播(entity="monthly")虽已发出,后台却没接。现已补上分支:任一端新增月度考勤,后台的月度考勤列表、看板「最近考勤」、首页看板立即刷新。 |
| 后台 · 系统初始化 (行为澄清) | 「🧨 系统初始化」= 清空全部业务数据(考勤 / 营位 / 用户ID / 回收站 / 日志,保留超级管理员账号与服务器配置),系统回到 0 数据的干净状态,可直接交付使用;不会自动生成任何数据,此后由使用者正式录入。(注:曾短暂加入「初始化后系统按节奏自动造数据」,经确认不符需求,已完全移除。) |
| 后台 · 系统初始化弹窗 | 弹窗内补充说明「初始化后系统回到 0 数据状态、可直接交付使用、不会自动生成数据」,避免误以为初始化会造演示数据。管理员账号与服务器 IP / 端口配置在初始化后仍保留。 |
| 文档 · 文件结构说明 | 项目文档新增「十四、文件结构说明」:逐一说明后端(火石营地后台管理系统Python/)与前端(frontend/、火石营地移动端/、火石营地uniAPP/)内全部文件的路径与用意。 |
| 模块 | 变更 |
|---|---|
| 后台 · 营位搜索框 | 营位列表页搜索框加宽到约三倍(660 px,窄屏自动收缩到 220 px 起步、不超出容器),输入即筛选营位编号 / 姓名,录入与查看更跟手。 |
| 后台 · 最近考勤 | 看板「最近考勤」由不设条数上限改为只展示最近 10 条(普通 / 小时 / 月度三类合并后按日期倒序取前 10 条);标题同步显示「🕒 最近考勤(最近 10 条)」,数量由后端 RECENT_ATT_LIMIT=10 下发,三端统一。 |
| 营位 · 全屏状态按钮 | 鸟瞰图全屏观看时,五个状态统计按钮(总计 / 已入住 / 空闲 / 已预订 / 维护,带数量)从「贴图片下沿」改为移到图片右上角(紧贴右上角工具列、避开旋转 / 缩放按钮),点一个即筛选该状态。后台 / H5 / uni-app 三端一致。 |
| 后台 · 保存拦截 (修复) | 修复「改营位数据 / 加电表二维码时弹『正在读取该营位的最新数据,请稍候再保存』、改不动」:根因是编辑框「正在读取」遮罩的 setCampLoading() 只清遮罩 DOM、没把全局标志 campModalLoading 复位,第一次打开后标志永远 true,后续每次保存都被拦下。现在 setCampLoading() 强制一起复位标志,保存不再被误拦。 |
| 全端 · 实时同步 | 任一端(后台 / H5 / uni-app)或后端数据变更,三端都自动刷新:后端写操作中间件 sync_middleware 新增覆盖回收站恢复 / 彻底删除 / 清空、系统初始化、Excel 导入、用户ID与权限、客户端访问 / 服务端配置等写接口,统一广播 entity="all"/"user"/"server" 让前端整体重拉;原已覆盖的营位 / UI 配置 / 三类考勤照旧。H5 与 uni 收到 all / refresh 后按当前模块整体刷新。 |
| H5 · 旋转按钮 (修复) | 修复「全屏鸟瞰图点旋转按钮没反应」:根因是按钮只写了 onclick="fsRotate()" 但函数从未定义,一点就抛 ReferenceError。现已补回 fsRotate()(顺时针转 90°、自动缩放到适配屏幕、视图回正中)。 |
| H5 / uni · 断线兜底 | 修复「改完数据前端 / 后端都不实时更新」:根因是数据加载以 WebSocket 是否连上(ready)为门槛,而 WS 在某些隧道下会悄悄断开却不触发 onClose,HTTP 仍正常,于是数据永远停在旧状态。现在 H5 / uni 改为以登录态为加载门槛,并加 15 秒兜底轮询 + 切回前台立即刷新;uni 端 WebSocket 增加心跳探活与带序号的重连(静默断线也能恢复推送)。 |
| 模块 | 变更 |
|---|---|
| 后台 · 全屏新增营位 | 鸟瞰图全屏观看时,右上角新增 「➕ 新增营位」 主按钮,功能与未全屏时完全一致:弹出「选状态」对话框(新增营位 / 已有营位两个页签)→ 选中状态后幽灵图标在全屏画布里跟随鼠标 → 左键点击落位 → 打开录入表单。全屏不会被退出,对话框与录入表单都浮在全屏层之上(z-index 已排好:全屏 100 < 弹窗 120 < 营位菜单 130 < 提示 140)。 |
| 后台 · 全屏缩放 | 全屏观看时滚动鼠标滚轮即可放大 / 缩小(0.5× ~ 8×),且以鼠标所在位置为原点——鼠标指着的那个点保持不动。右上角实时显示当前倍率(如 150%),另有 「⟲ 重置视图」 一键回到 100% 居中。 |
| 后台 · 全屏拖动 | 全屏观看时按住鼠标左键拖动即可平移地图(空白处、营位图标上都能拖)。平移带夹取保护:地图至少留 1/4 屏在视野里,不会被拖出屏幕外找不回来。拖动结束后的抬手动作不会误触发「落位」或「打开营位详情」。 |
| 营位 · 图标大小 (精确到单个) | 「📏 调整图标大小」滑块由「改一次全体营位」改为 只改当前右键选中的那一个营位,其它营位不受影响。面板会说明当前状态:「当前跟随全局 N%,拖动即单独设置」或「仅此营位(全局默认 N%)」。数值单独存在该营位的 pin_scale 字段(null = 跟随全局),三端(后台 / H5 / uni-app)按同一个比例渲染;「↺ 恢复默认大小」 即写回「跟随全局」。普通鸟瞰图与全屏鸟瞰图虽然底图宽度不同,但「图标 ÷ 底图宽」的比例严格一致(各自按自己那张底图算,不会互相带歪)。 |
| H5 / uni-app · 全屏状态筛选 | 全屏鸟瞰图里新增一排状态统计按钮(总计 / 已入住 / 空闲 / 已预订 / 维护,带数量),贴在图片下沿叠在图上;点一个即只显示该状态的营位图标,其余隐藏,再点一次取消;选中的按钮高亮。地图缩放 / 拖动时按钮条跟着图片走。 |
| H5 · 全屏拖动 (修复) | 修复「全屏放大后鼠标不能随意移动地图」:根因是在图标上按住拖动时,浏览器把选中的文字当成了原生拖放,触发 dragstart 后立刻 pointercancel,平移只走一步就断(实测拖 90 px 只走 15 px)。现在舞台与图标禁用文字选中(user-select:none)、拦截 dragstart / selectstart,并把拖动监听改挂到 window 上(即使指针捕获丢失也能继续拖);同时加双指捏合缩放与平移夹取。 |
| 开发 · 自检工具 | 新增 _verify16.py:临时副本库 + 临时端口 + 真实 Chrome 全量校验(含 4 个端到端脚本:后台全屏新增 / 缩放 / 拖动 / 单营位,H5 全屏筛选 / 拖动回归 / 单营位)。另修掉 _e2e 里若干条已过期的断言(全局滑块改为单营位后,旧的「改一次全体」断言不再成立;图标 px 值在两个视图本就不同,改为比对「图标 ÷ 底图宽」的比例)。 |
| 模块 | 变更 |
|---|---|
| 营位 · 图标比例 (修复) | 手机端(uni-app / H5)的营位图标明显比后台大,已修复。根因:算出的基准直径被强行夹在 18~76 px,手机底图只有 364 px 宽时本应 11.6 px,被抬到 18 px 后比例从 3.2% 变成 4.95%(后台大屏仍是 3.2%)。现在取消这层固定夹取,只做 4~420 px 的可见性兜底;圆内字号、编号标签字号也全部改为按直径成比例(不再写死 11 px / 10 px),2 px 白边改为含在直径内(box-sizing:border-box)。三端「直径 ÷ 底图显示宽度」严格相等。 |
| 营位 · 缩放区间 | 后台「📏 调整图标大小」滑块由 50%~200% 放宽到 20% ~ 300%(默认 100%):底图大时可细调到 20% 免得图标遮挡,手机小屏可放到 300% 仍看得清。区间与比例常量由服务端下发(/api/ui-config),三端共用一份。 |
| 营位 · 小图标可读性 | 图标直径 < 25 px 时自动隐藏编号标签(此时标签字号不足 7.5 px,看不清反而显得比图标还大);直径 ≥ 25 px 恢复正常显示。三端同一条规则。 |
| 营位 · 全屏图标 | 全屏观看时按全屏底图宽度重新计算直径:后台用布局宽度(offsetWidth,不受缩放影响),H5 同样按量取宽度,uni-app 按 92vw 单独算一份 —— 全屏放大后图标与地图仍是同一个比例,且不会因缩放被二次放大。 |
| 开发 · 自检工具 | 新增 _verify15.py:直接抽出三端源码里的计算公式逐项求值对比(11 种底图宽 × 6 档缩放 = 66 组必须完全相等,并校验「直径 = 底图宽 × 3.2% × 缩放比」),以后任一端改歪都会立刻报错。另修掉 _undeclared.py 的两类误报(逗号列表声明、箭头函数参数),误报从 49 条降到 2 条。 |
| 模块 | 变更 |
|---|---|
| 营位 · 新增营位 (修复) | 卡片模式与鸟瞰图模式点「➕ 新增营位」都没反应,已修好。根因是编辑对话框「正在读取」遮罩用的 setCampLoading() 只被调用、从未定义,一点「新增营位」就抛异常,弹窗根本打不开;同时「已有营位」定位用的 campPlaceExistingId、图标大小用的 pinScale 两个变量也漏了声明。三处补齐后,卡片新增 / 鸟瞰图新增 / 已有营位定位 三条路径全部恢复正常。 |
| 营位 · 已有营位数量 | 「📍 已有营位」页签上新增数量徽标(= 还没有鸟瞰图坐标的营位数),列表上方同步显示「共 N 个营位还没有鸟瞰图坐标」,一眼就知道还剩几个待定位;新增/定位/编辑/删除后数字自动刷新。 |
| 营位 · 网络误报 (修复) | 给没有坐标的营位在地图上定位时,会弹「保存坐标失败,请检查网络」,但坐标其实已经写进数据库了。根因:保存成功后的「界面刷新」这一步抛了 JS 异常,被同一个 .catch 当成了网络故障。现在网络错误与界面刷新错误分开处理:只有真正的请求失败才提示网络;界面刷新失败只在控制台留一行诊断,不再误导用户重复操作。 |
| H5 · 全屏点击图标 | 全屏查看鸟瞰图时,点营位图标现在能弹出营位详情对话框(点完自动退出全屏)。根因:全屏舞台在按下时调用了 指针捕获(setPointerCapture),浏览器会把随后的「点击」派发给舞台本身,图标上的 onclick 永远收不到;现改为在抬手时按坐标做命中测试自己判断点到了哪个图标。 |
| uni-app · 全屏点击图标 | 同上问题与修法:手机端只在图标上挂 @tap 并不可靠(父级 touchstart 抢事件、手指轻微抖动都会把 tap 吞掉),现在改为在 touchend 时按「起止点位移很小」判定为点击;@tap 仍保留作为兜底,两者去重,保证只打开一次详情。 |
| 开发 · 冒烟测试隔离 | 冒烟测试改为先在临时副本库上跑(SQLite backup API 整库复制,被测进程的 DB_PATH 指向副本),测完自动删除副本;此前直接在真实库上跑,会在「删除的数据」与操作日志里积累 SMOKE1 测试残留——这些残留已一并清理(清理前已备份数据库)。 |
| 模块 | 变更 |
|---|---|
| 营位 · 图标大小 | 鸟瞰图里右键点击营位图标,菜单新增 「📏 调整图标大小」:点击弹出竖向滑块(50% ~ 200%,默认 100%;本轮已放宽到 20% ~ 300%),拖动即可实时改变全部营位图标的大小(本轮已改为只改当前右键选中的那一个营位),并带 「↺ 恢复默认大小」。数值保存在服务端(ui_config.pin_scale),后台 / H5 / uni-app 三端统一生效;在后台调完,手机端立即同步,无需手动刷新。图标直径仍按「底图显示宽度 × 3.2%」计算后再乘该比例,最终夹在 18 ~ 160 px(本轮已取消这层固定夹取,改为只按比例 + 4~420px 兜底)。 |
| 营位 · 详情右栏 | 详情对话框右栏原先把「📜 入住记录」和「🕓 变更记录 / 操作日志」挤在同一个滚动框里;现拆成 两个页签——点「📜 入住记录」只看入住时间线,点「🕓 变更记录 · 操作日志」只看操作日志,点击可切换并高亮当前选中项,两个页面各自独立滚动,不再互相干扰。 |
| 营位 · 新增营位 | 鸟瞰图模式点「新增营位」弹出的对话框拆成两个页签:「新增营位」=原流程(选状态 → 图标跟随鼠标 → 左键点击落位 → 写库 → 录入信息);「已有营位」=列出尚未设置鸟瞰图坐标的营位,选中一个后直接在图上点击定位,只写坐标、不改动其它信息(用于给历史上没标过位置的营位补位置)。 |
| 营位 · 空闲保留 | 把营位设为 「空闲」 时,除原有的 编号 / 电表二维码 / 入住记录 / 操作日志 外,现在连鸟瞰图坐标(pos_x / pos_y)也一并保留:图标仍待在原来的位置,重新启用时无需再摆放。其余属性(姓名 / 电话 / 入住类型 / 入住·退房日期 / 消费金额 / 备注 / 剩余天数)照旧清空。 |
| 营位 · 状态一致性 (修复) | 修复「营位卡片显示已入住,但点编辑或查看详情却是空闲」的问题:根因是退房日期已经是过去时间的入住记录(例如补录历史入住),一打开详情就触发「到期自动释放」被立刻清成空闲。现在保存时若退房日期已过期,会自动标记为「不参与到期释放」并在保存后弹出提示;同时营位释放动作立即全端同步,卡片、鸟瞰图与详情三处状态保持一致。 |
| 模块 | 变更 |
|---|---|
| 考勤看板 · 最近考勤 | 表头由「编号 / 姓名 / 日期 / 时长 / 工作内容」改为「编号 / 姓名 / 日期 / 考勤类型 / 工作内容」:普通考勤、小时考勤、月度考勤三类合并到同一张表,考勤类型列用彩色徽标区分;按日期倒序,不设条数上限。编号仍为该记录在自己那一类里的固有编号。 |
| 营位 · 鸟瞰图交互 | 移除工具栏「✥ 移动编辑」按钮及其「所有图标旁出现按钮」的旧模式;改为 右键单击营位图标弹出菜单(移动 / 删除)。左键点击图标的功能保持不变(查看详情)。「移动」进入跟随鼠标状态、左键点击落位写库;「删除」走确认对话框。菜单贴边自动收进窗口,点别处 / Esc / 滚动即收起。 |
| 营位 · 图标比例 | 营位图标直径改为 = 底图显示宽度 × 3.2%(夹在 18~76 px;该固定夹取已在本轮取消,它是手机端图标偏大的根因),后台、H5、uni-app 三端使用同一比例常量:手机端不再出现图标相对地图过大、互相挤压的观感;图标内的文字另有最小字号兜底,保证手机上仍可辨认。 |
| 营位 · 底图实时同步 | 后台上传 / 更换 / 清除鸟瞰图底图后,正在看鸟瞰图的手机端(H5 / uni-app)立即重新拉取底图并重绘,无需手动刷新(底图变更走 WebSocket sync(campsite, map))。 |
| H5 · 鸟瞰图全屏 | 鸟瞰图右上角新增「⛶ 全屏」按钮:全屏观看时右上角变为 「⛶ 全屏返回」,其下依次是 + / -(放大 / 缩小)与 ⟳(旋转 90°);旋转后自动缩放使图片完整适配屏幕;按住左键(手指)可拖动图片;点全屏里的营位图标同样弹出营位详情对话框。 |
| H5 · 只读查看 | 鸟瞰图查看视图下 右下角「+」按钮自动隐藏,该视图只能查看、不能编辑与删除(编辑 / 删除只在卡片模式可用);并补齐了「点图标 → 营位详情」对话框(此前该入口缺失,点击图标会报错无反应)。 |
| uni-app · 鸟瞰图全屏 | 与 H5 一致的全屏观看(旋转 / 放大 / 缩小 / 按住拖动 / 全屏返回);鸟瞰图视图隐藏右下角「+」;手机返回键优先关闭全屏再退出其它弹窗。 |
| 模块 | 变更 |
|---|---|
| 营位 · 详情右栏 | 📜 入住记录 与 🕓 变更记录 / 操作日志 两个标题固定吸顶:滚动右栏时标题不动,只滚动下面的数据,长列表也能随时看清当前看的是哪一段。 |
| 日志 · 二维码 | 日志 / 操作动态里不再出现整串二维码图片数据:电表二维码是一张约 1.5 MB 的图片(data:image/png;base64,…),此前「恢复数据」等日志会把整段 base64 原样写进去,导致历史记录、实时动态与页面渲染明显卡顿甚至卡死。现在写入前统一压成「已上传(图片 1.51 MB)」这类短描述。 |
| 日志 · 通用兜底 | 任何超过 200 字的文本值在日志里自动截断为「前 200 字 + …(共 N 字符)」;回收站列表接口也不带图片数据(详情接口仍返回完整数据,恢复不受影响);前端渲染再做一层超长值兜底。 |
| 历史数据清理 | 已把数据库里既有的一条 1.51 MB 日志压缩到 687 字节,数据库体积由 4.85 MB 降到 3.10 MB(处理前已自动备份到 backups/)。 |
| 模块 | 变更 |
|---|---|
| 主页面 · 登录IP | 历史记录里的登录 / 操作 IP 显示乱码修复:后端统一解码并去掉协议头,只保留 192.168.1.2:8766 形式;旧记录读取时自动纠正。 |
| 主页面 · 历史记录 | 取消全部条数上限(原 200 / 150 / 40 条限制移除):历史记录、实时操作动态全量显示与保留;服务启动时回填历史动态。 |
| 营位 · 鸟瞰图 | 未上传底图时不再渲染营位图标;未上传提示由「居中的图标 + 两行文字」改为一行浅灰文字,去掉中间那个「孤零零的图标」。 |
| 营位 · 入住记录 | 取消详情页「录入新的入住记录」表单;入住记录统一从编辑营位产生——入住日期 / 退房日期一旦变化并保存,自动追加一条入住记录(写库 + 写日志,含新旧日期对比);日期未变不重复追加。 |
| 营位 · 详情版式 | 去掉左栏「设为四种状态」的按钮;左栏只显示营位全部信息;原左栏的「变更记录 / 操作日志」整体移到右栏入住记录下方;右栏数据多时内部显示上下滚动条。 |
| 回收站 | 恢复数据后前端立即联动刷新:回收站、对应模块表格、首页看板与历史记录同时更新(后端恢复响应回传 entity)。 |
| 考勤 · 排序 | 三类考勤的「日期」列新增排序:与「编号」列一样可点击切换升 / 降序,未排序列显示 ⇅;仍只改行序、不重排编号(三端一致)。 |
| 后台 · 操作日志 | 日志摘要不再截断(原先只显示前 6 / 8 项),变化过的属性全部写入摘要。 |
| H5 端 | 所有弹窗宽度在宽屏下与背后的数据表格一致(含新增 / 编辑工时);营位页搜索框后新增「🗺️ 鸟瞰图」按钮,点击查看底图与营位位置(只读),点图标看营位详情。 |
| uni-app 端 | 修复营位卡片「编辑」按钮点击无反应(入住类型下拉的索引变量未声明导致运行时报错);营位页搜索框后新增「🗺️ 鸟瞰图」按钮(只读查看 + 点图标看详情)。 |
| 模块 | 变更 |
|---|---|
| 营位 · 到期 | 新增营位到期自动释放:到期自动变空闲,仅保留 编号 / 电表二维码 / 入住记录,并写操作日志。 |
| 营位 · 日志 | 操作日志记录到属性级:哪个营位的哪条属性、修改前与修改后的对比全部写明,详情里逐条展开。 |
| 营位 · 详情 | 「录入新的入住记录」移到上方、「入住记录」移到下方;修复录入区输入框超出对话框宽度的问题;新增 设为四种状态 的切换按钮。(该按钮与录入表单已在后续需求中移除) |
| 营位 · 字段 | 三端统一:空闲时只显示 编号 / 姓名 / 状态 / 电表二维码;其它状态显示全部 10 项;字段顺序统一;保存为「空闲」清空其余属性(保留 编号 / 电表二维码 / 入住记录 / 操作日志)。 |
| 营位 · 鸟瞰图 | 底图 + 百分比定位;新增流程(选状态 → 跟随图标 → 点击落位 → 写库 → 录入);移动编辑(移动位置 / 删除营位);坐标与图片尺寸解耦。 |
| 营位 · 卡双模式 | 卡片模式 / 鸟瞰图模式切换按钮置于搜索框下、选中高亮;「✥ 移动编辑」仅鸟瞰图模式显示;移除搜索框后的刷新按钮。 |
| 考勤 · 详情 | 去掉「部门 / 工时」;「工号」改为「编号」并显示真实固有编号;关闭按钮移到右上角、标题固定、仅内容区滚动。 |
| 考勤 · 看板 | 月份统计前新增 「📊 合计」 按钮,一键查看该考勤所有月份的总条数。 |
| 后台 · 界面 | 侧栏可折叠(只留图标、悬停显示名称);系统模块五个按钮与全部菜单项图标统一对齐;确认/提示统一为居中对话框。 |
| H5 端 | 营位对话框「名称」改「姓名」;对话框宽度调整为与营位卡片同宽;字段规则与后台一致。 |
| uni-app 端 | 修复二维码下载按钮无反应(含相册授权引导);手机返回键优先关闭弹窗;营位弹窗字段与规则同后台。 |
后续任何功能变更,都应同时更新本文件与后台「系统文档」中的内嵌副本(两份内容保持一致)。
本节列出后端(火石营地后台管理系统Python/,含同目录下的游客自助入住登记页 游客入住信息自助录入/)与前端(后台界面 frontend/、手机 H5 火石营地移动端/、uni-app 火石营地uniAPP/)内的全部文件(含子目录,路径均为相对项目根目录)。带 _ 前缀的文件与 _e2e/ 目录都是开发自检工具,不参与程序运行,交付时可保留或移出。
火石营地后台管理系统Python/)| 文件(相对项目根目录) | 用意 |
|---|---|
火石营地后台管理系统Python/app.py | 主程序:FastAPI 后端 + WebSocket 实时同步 + SQLite 数据层 + pywebview 桌面窗口。全部接口、业务逻辑(考勤 / 营位 / 鸟瞰图 / 用户权限 / 回收站 / 系统设置 / 初始化)、定时任务(营位到期检查)都在此文件。 |
火石营地后台管理系统Python/media_compress.py | 全景图(照片 / 视频)上传压缩:照片按 EXIF 方向摆正 → 最长边缩到 PHOTO_MAX_SIDE=1920 → 透明底垫白 → 重编码 JPEG(质量 82,也可保留 WebP);GIF 不压(会丢动画,由调用方跳过)。视频用 ffmpeg 转码成更小的 MP4(H.264 / yuv420p / 宽度 ≤ VIDEO_MAX_WIDTH=1280 / CRF=VIDEO_CRF=28 / +faststart),体积通常降 50–80%;拿不到 ffmpeg 时 is_ffmpeg_ready() 为假,由 app.py 回退成「存原文件 + MP4 faststart 无损重排」,保证手机端至少能起播。 |
火石营地后台管理系统Python/启动.pyw | 日常启动入口(推荐,双击它):无 cmd 黑框、也没有独立的启动小窗口——静默检查依赖后直接拉起 app.py,弹出的就是后台管理系统窗口;「正在启动 / 服务器启动成功」提示已做成后台页面内部界面。依赖缺失时静默安装,失败弹系统消息框,详细报错写入同目录 start.log。 |
火石营地后台管理系统Python/start.py | 排错入口(保留控制台):完整打印 app.py 的输出,只有出问题需要看报错时才用。 |
火石营地后台管理系统Python/start.bat | Windows 一键启动脚本:内部转调 启动.pyw(同样无 cmd 黑框)。 |
火石营地后台管理系统Python/requirements.txt | Python 依赖清单(fastapi、uvicorn、pywebview、openpyxl 等)。 |
火石营地后台管理系统Python/README.md | 项目说明:功能模块、技术栈、运行方式、接口一览。 |
火石营地后台管理系统Python/camp_data.db | 运行时数据库(SQLite):全部业务数据(考勤 / 营位 / 用户 / 日志 / 回收站 / 服务器配置)。⚠️ 交付前用「系统初始化」清空,即回到 0 数据干净状态。 |
火石营地后台管理系统Python/backups/camp_data_*.db | 数据库备份快照(自动/手动备份)。属历史归档,可删或另行保存,不影响运行。 |
frontend/)| 文件(相对项目根目录) | 用意 |
|---|---|
火石营地后台管理系统Python/frontend/index.html | 后台管理端界面(单文件,内联 CSS/JS):桌面浏览器访问,含考勤、营位、鸟瞰图(含全屏)、用户ID、系统设置、回收站等全部页面。 |
火石营地后台管理系统Python/frontend/system-doc.html | 项目文档的内嵌副本:后台「系统 → 系统文档」打开的就是它,内容须与根目录 火石营地管理系统-项目文档.html 保持一致。 |
游客入住信息自助录入/)| 文件(相对项目根目录) | 用意 |
|---|---|
火石营地后台管理系统Python/游客入住信息自助录入/index.html | 游客自助入住登记页(单文件 H5,内联 CSS/JS)——给游客用的、不需要账号密码。挂载为 /guest(/guest/ 亦可),与后端同源直连。页面分两步:① 选营位:鸟瞰图上只画「空闲」营位的图标(已被占用的营位连编号、姓名都不显示),点图标即选中并可高亮;同时提供「营位编号」下拉列表(选项全部由系统下发,只能选不能编辑),两者双向同步;底图可放大 1× / 1.5× / 2.2× / 3× 便于挑选,图标另加了 ≥46px 的透明点击区方便手机点。② 填信息:姓名、联系电话、入住日期(日期控件)、退房日期(日期控件)、入住类型(常驻 / 临时)。提交前弹确认框复核,确认后调 POST /api/guest/checkin 直接写入该营位的数据(状态置「已入住」),并刷新可选中列表。图标尺寸沿用四端统一公式「底图显示宽 × pin_ratio × pin_scale%」,随后台全局「调整图标大小」一起变化。 |
_*.py)| 文件(相对项目根目录) | 用意 |
|---|---|
_frontcheck.py | 抽取三端内联 JS 做 node --check 语法检查 + DOM id 重复 / 缺失引用排查。 |
_mod_e2e.py | 数据门控 + 自定义模块的隔离回归测试:在真实库的临时副本上跑(不碰真实数据),覆盖 启动/停止门控、模块增删改、模块数据增删改查、文件上传、权限清单动态并入,共 22 项断言。 |
_perm_e2e.py | 权限保存 + 系统初始化的隔离回归测试(同样跑在真实库临时副本上):① 勾选全部模块保存后必须真的存成 admin(回读 + 列表接口一致);② 新增自定义模块后只提交老模块 key 也不把管理员降级;③ 系统初始化清空业务数据但 用户ID 与其权限完整保留、原账号仍能登录。共 19 项断言。 |
_fieldcheck.py | 对比三端「新增 / 编辑营位」对话框的字段顺序是否一致(三端一致原则的自动校验)。 |
_undeclared.py | 扫描内联 JS:找出「有赋值、但从未声明」的标识符(strict 模式下会抛 ReferenceError)。 |
_check_ui.py | 校验界面静态挂载 + 关键标记存在 + CORS 头。 |
_smoke.py | 冒烟测试:临时端口启动服务、验证接口与页面,跑完自动关闭、不占用用户端口(已隔离副本库)。 |
_guesttest.py | 游客自助入住登记 的隔离验收(整库复制到临时副本 + 临时端口,真实数据零污染):/guest 页面可访问、/api/guest/enter 记「游客登录」、9 种表单校验、正常登记落库(姓名 / 电话 / 日期 / 入住类型 / 状态 / 剩余天数 / 入住记录)、重复登记被拒、2 秒防连点节流、变更日志为「游客 / guest / 原值→改后值」、/api/history 展示。共 49 项。 |
_pwtest.py | 「修改密码」的隔离验收(整库复制到临时副本 + 临时端口 8794,测试账号 ZZPWD1 / ZZPWD2 只建在副本里,跑完删副本):接口层 17 项(普通账号自助改密不被「用户ID管理」权限拦下、原密码错 / 新密码太短 / 与原密码相同 / 用户ID 不存在均被拒、改完新密码可登录且旧密码失效、后台用户列表里的密码同步为新密码、变更日志含操作人与原值→新值、管理员重置接口仍被权限保护)+ 拉起 _e2e/run_password.js 走真 Chrome 全流程 + 核对真实库零污染。用法 python _pwtest.py。 |
_autologintest.py | H5 登录页「自动登录」的隔离验收(整库复制到临时副本 + 临时端口 8793,在副本里建测试账号 ZZAUTO1,跑完删副本):拉起 _e2e/run_autologin.js 走真 Chrome 全流程,并核对真实库零污染(真实库里没有该测试账号、也没有一条属于它的登录日志)。用法 python _autologintest.py。 |
_serve.py | 只启动 uvicorn(不弹桌面窗口),用于在浏览器里验证界面。 |
_perfserve.py | 隔离性能验证服务器:用 SQLite backup() 把整库复制到副本库,再在指定端口起一个实例,全部写操作只落在副本上(真实库与正在运行的 9876 零污染)。用于跑 _e2e/perf_login.js / run_mapslim.js / run_qrperf.js 这类需要写登录日志或改底图的测试。不改防火墙、不注册运行期状态。用法 python _perfserve.py <端口> <副本库路径>。 |
_maptest.py | 鸟瞰图底图「改图 / 清图」往返验证(打临时副本库,真实库零污染,共 17 项):上传新底图 → v 版本号变化且图片端点返回的正是新图;换回原图 → 版本号按内容复原(不掺时间戳);清除 → has_map=false 且图片端点 404(前端走空态而不是破图);恢复原图 → 与初始状态完全一致;并核对变更日志两条。用法 python _maptest.py [http://127.0.0.1:9879] [副本库路径]。 |
_v6check.py | IPv6 入站可达性一键自测:双栈监听一个临时端口(setsockopt(IPV6_V6ONLY,0) + bind(("::",port))),自动从 ipconfig 里挑出本机全局 IPv6 地址(排除 fe80 链路本地、fc/fd 私有与临时地址),并打印出可直接在手机上打开的 URL;页面会回显来访者地址,用来确认「确实是从公网 IPv6 进来的」。用途:判断光猫 / 路由器的 IPv6 防火墙有没有放行到你电脑(本机实测:电信 CGNAT 下没有公网 IPv4,但有真公网 IPv6 240e:36e:70f:8fd0::/64)。用法 python _v6check.py [端口],测试时手机须用蜂窝流量并关掉 WiFi。测完 Ctrl+C 退出即可,不写任何数据。 |
_domcheck.py | 域名接入体检(只读、不写库、不改域名):列出本机出口 IPv4 与全局 IPv6(公用 / 临时分开,并标注「首选寿命 0s = 已废弃」),查 A / AAAA / CNAME / NS / TXT 与解析耗时(系统解析器连查 3 次 + 公共 DoH),对每个解析出的地址做逐端口 3 次 TCP 建连与 HTTP/HTTPS 探测,最后自动判定,能直接抓出 「A 记录 = 本机出口 IPv4」这个 CGNAT 陷阱(解析正常但外部必然连不上)、「只有 A 没有 AAAA」、「AAAA 与本机公用地址不一致」等问题。用法 python _domcheck.py <域名> [端口 …],默认测 80 / 443 / 9876。 |
_run_verify.py | 批量运行全部 _verify*.py 的入口(带数据库备份还原)。 |
_test_camp.py | 在无 GUI 环境下 mock 掉 webview / uvicorn 后导入 app,做接口测试。 |
_shrink_log_blobs.py | 一次性维护脚本:把历史日志里遗留的整段 base64 图片压缩成短文本。 |
_verify2.py | Excel 多工作表导入导出 / 变更日志 / 时段工时换算(带 DB 备份还原)。 |
_verify3.py | Excel 多表读写、变更日志、时段工时换算的后端验证。 |
_verify4.py | 营位二维码单独上传接口 + 营位列表手机号完整返回。 |
_verify5.py | 用户ID 权限(普通账号仅查看)+ 操作动态明细(修改前后值)。 |
_verify6.py | 用户ID「修改管理权限」(角色 + 各模块可编辑 / 仅查看)与接口鉴权。 |
_verify7.py | 鸟瞰图坐标 / 入住记录 / 剩余天数换算 / 入住类型 / 底图 相关后端能力。 |
_verify8.py | 营位到期自动释放 / 空闲清空规则 / 操作日志逐属性对比。 |
_verify9.py | 客户端 IP 规范化 / 取消条数限制 / 入住记录随日期联动 / 恢复返回 entity。 |
_verify10.py | 日志与动态里的二维码等大字段已压短(不再写入整段 base64)。 |
_verify11.py | 看板「最近考勤」三类合并 + 类内固有编号 + 无条数上限。 |
_verify12.py | 图标大小配置 / 到期释放豁免 / 空闲保留坐标 / 三端接线(临时副本库)。 |
_verify13.py | 新增营位 / 已有营位数量 / 状态一致性 & 误报网络修复 + 全屏点图标弹详情接线。 |
_verify14.py | 全屏鸟瞰图静态校验(保持全屏看详情 / 滚轮缩放 / 空处右键菜单)。 |
_verify15.py | 营位图标「尺寸比例」三端是否真正一致 + 缩放区间是否放宽到 20%~300%(逐项求值对比)。 |
_verify16.py | 综合性校验:全屏新增营位 / 滚轮缩放拖动 / 单营位图标大小 / H5+uni 全屏状态筛选(副本库 + 临时端口 + 真 Chrome)。 |
_qr_map_e2e.py | 二维码压缩 + 鸟瞰图底图下载 + 全景图(照片 / 视频)下载的隔离回归(真实库临时副本 + TestClient,用完即删):手机原图级二维码经三个写入口入库后体积必须缩到 1/4 以下且仍是合法图片、写接口响应不再回传整段 base64、底图下载回原图且带 Content-Disposition、uni 端 multipart 直传(POST /api/upload/qr)返回的 data URL 已压缩且能直接落库。第 8 组专测全景图下载端点:照片 / 视频都能下、Content-Type 对、attachment + 中文 filename* 对、后缀与实际落盘文件一致、媒体 / 营位对不上时回 error(这一组的上传会真的写盘,所以先把 MEDIA_DIR 指向临时目录)。当前 51 项全过。⚠️ 压缩基准由脚本自己种:启动时往副本库写一张 1800×2250 的「照片级」PNG( _photo_like_png()),并显式关掉存量收敛(A.QR_IMG_LEGACY_BYTES = 1<<60)—— 否则 init_db() 一启动就把它压成 ~130KB,基准没了、压缩比断言全部失真(收敛由 _e2e/run_qrfast.js 专测)。⚠️ 别拿「纯随机色块」当基准:那是 JPEG 的最坏情况,实测只压到 1.68×,会把「缩到 1/4 以下」逼红;兜底生成器已换成同样的照片级实现。 |
_qr_map_mutate.py · _qrup_mutate.py | 上面那条的变异测试:把后端 shrink_qr_data_url() / 前端的图片压缩改成「原样返回」,断言隔离回归必须 FAIL(证明那些断言不是空转),跑完自动还原源文件。 |
_size_e2e.py | 营位尺寸(size)字段的隔离回归:新增 / 编辑 / 释放营位 / 回收站恢复各路径下 size 是否按「缺省 = 不动」正确保留。 |
_recompress_media.py | 一次性维护脚本:把 media/ 里历史遗留的全景图(照片 / 视频)批量重新压缩一遍。 |
_extract_js.py · _extract_vue.py | 把 HTML 里的内联 JS / .vue 的 <script setup> 抽成单独文件,好交给 node --check 做语法自检。 |
_shots_make.py · _shots2_make.py | 视觉核对脚本:抽真实 <style> 配等价静态骨架,用无头 Chrome 截图人工确认布局。 |
_*.js、_e2e/)| 文件(相对项目根目录) | 用意 |
|---|---|
_jscheck.js | 用 node 对前端内联 JS 做语法检查。 |
_domcheck.js | 用 node + jsdom 检查页面 DOM id 引用是否缺失。 |
_vuecheck.js | 用 node 检查 uni-app .vue(script setup)语法。 |
_e2e/cdp.js | 极简 CDP 客户端:启动本机 Chrome → 连接 DevTools → 发真鼠标 / 滚轮 / 右键事件(全部只读,不点保存 / 删除)。 |
_e2e/run_pinratio.js | ① 实测「营位图标直径 ÷ 底图显示宽度」的比例,在后台桌面视口与手机视口是否完全一致(比例常量从 /api/ui-config 现读,不写死);② 实测编号胶囊尺寸:字号必须 = max(6, 图钉直径 × .26),且胶囊不许比图钉胖太多(防「编号太大挡住地图」回归),在桌面 100% / 300% 与手机 390×844 的 100% / 300% / 50% 共 5 个场景各量一次。⚠️ 真实库里营位可单独设 pin_scale(内联 --pin 盖掉画布级),脚本会只在内存中清掉这些覆盖量,否则「直径 = 底图宽 × 全局缩放比」这条断言测的其实是那个营位自己的比例。用法 node run_pinratio.js http://127.0.0.1:端口。 |
_e2e/run_admin_fs.js | 后台「鸟瞰图全屏观看」端到端验证(真 Chrome + 真鼠标事件)。 |
_e2e/run_admin_fs2.js | 后台全屏第 2 组:全屏「➕ 新增营位」全流程 / 滚轮缩放 / 左键拖动 / 单营位图标大小。 |
_e2e/run_h5_fs.js | H5「鸟瞰图全屏观看」端到端验证(含滚轮)。 |
_e2e/run_h5_fs2.js | H5 全屏第 2 组:叠在图片上的状态统计按钮筛选 + 拖动回归。 |
_e2e/run_h5_fs3.js | H5 全屏第 3 组:跟随手机横屏(CDP 视口模拟 390×844 ↔ 844×390)——⟳ 按钮已删除、竖版底图横屏自动转 90°、「全屏返回」+5 个状态按钮同排贴右上角、退出全屏旋转/缩放复位。13 项。 |
_e2e/run_attsort.js | 考勤默认按日期排列 + 序号与日期一一对应 + 新增置顶序号 + 同一天多条不出现断层端到端验证(admin + H5):注入「先录 9/29、后补录 9/19」的真实形态,校验补录记录自动落到 09-19 的位置、序号=日期升序位次(09-19→1 / 09-29→2,降序读 2、1,细节弹窗口径一致)、点「编号」表头与点「日期」表头完全等价(两列表头箭头同步、无 ⇅ 残留)、新增置顶 2 秒置顶行显示它自己的固有编号(第 3 条 → 3)且 2 秒后自动归位;另有一段「真实形态」用例(照抄 /api/attendance 返回顺序:12 条、10-01 一天 6 条)断言升序 1…12 / 降序 12…1 全程单调、与日期逐行同向。40 项。用法 node run_attsort.js http://127.0.0.1:端口。 |
_e2e/run_perm.js | 后台「修改权限」闭环端到端验证(整库副本 + 独立端口 + 真实 Chrome,真实数据零污染):登录 → 进「用户ID管理」→ 打开「修改权限」→ 勾选 → 保存 → 列表 chips 必须变「可编辑」且接口回读 role=admin;反向取消一项后回落到「仅查看」;两个权限勾选框容器(新建表单 / 修改弹窗)互不污染;全程 0 个 JS 异常。20 项。 |
_e2e/perm_mutate.js | 上面那组断言的 变异测试:把 renderPermChecks / readPermsFromBox 临时改回旧的「固定 id + getElementById」写法(即「勾了可编辑仍保存成仅查看」的成因),再跑一遍 run_perm.js —— 必须出现 FAIL,否则说明断言空转。跑完无条件还原源文件。 |
_e2e/run_video.js | 全景图视频播放端到端验证:封面 ▶ 是否可见可点 → 全屏播放器是否弹出 → 视频是否真的解出时长 / 分辨率(能播的硬证据)→ 关闭后是否暂停并释放地址。用法 node run_video.js h5|admin。 |
_e2e/check_mp4.js | 全景图视频文件健康检查:用 Range 取文件头 2KB 断言 moov 在 mdat 之前(faststart,决定手机端能不能开始播放),并用真实 Chrome 断言仍可解码(readyState≥1 / 时长 / 分辨率,防重排写坏偏移)。用法 node check_mp4.js <文件名>。 |
_e2e/run_guest.js | 游客自助入住登记页的真实浏览器端到端验证(临时库副本 + 临时端口起服务,跑完删副本):页面与「游客登录」标识、鸟瞰图只画空闲营位(已入住营位与其客人姓名都不出现)、下拉选项由系统下发、图标尺寸严格等于「底图宽 × pin_ratio × pin_scale%」且可点区 ≥46px、真鼠标点图标与用下拉两条选中路径、缩放后比例仍一致、表单校验 / 确认弹窗 / 提交成功、登记后营位从可选中消失、库里数据与后台历史、全程 0 个 JS 异常。共 44 项。用法 node run_guest.js。 |
_e2e/run_access.js | 后台「服务器访问地址」列表端到端验证(真 Chrome + CDP)。不带参数时自起隔离实例:整库副本 _access_e2e.db + 临时端口 8797 + HS_E2E_DRY_OPEN=1(服务端只回报「将要启动哪个浏览器」,绝不真的弹出浏览器窗口),跑完删副本;带 baseURL 参数则连那个现成实例,并自动跳过「真的换个浏览器打开」那一步。验证:走真实登录 → 从侧栏点进页面 → 每行的序号 / 名称 / 访问地址与 /api/client-access 逐条比对;左键仍是 <a target="_blank">+rel=noopener、无 onclick、user-select:none、每行带 data-ca-no;真鼠标右键弹菜单(标题=行名、两项文案、不出窗口)→ 「复制地址」复制到的确实是该行地址且给出提示 → 「在其它浏览器中打开」列出本机浏览器(与 /api/local-browsers 逐条一致、默认浏览器排最前)→ 选一个走 POST /api/open-in-browser;服务端校验(清单外网址 / 错误编号)必须被拒;真鼠标点一下地址确实新开标签且目标正确、原页面不被导航走;Esc 收起菜单与面板;0 个 JS 异常。共 56 项。用法 node run_access.js(或 node run_access.js http://127.0.0.1:9876)。⚠️ 它曾整体假红 11 项:侧栏选择器写的是早已改名的 serveraccess(实际 serverip),且「所有地址都以 / 结尾」与后来的 APP 裸地址行(http://<IP>:<端口>,不带路径)冲突 —— 页面改名 / 加行后没同步测试,已一并修正。 |
_e2e/run_password.js | H5「🔑 修改密码」端到端验证(真 Chrome + 真鼠标事件):顶栏入口可点、弹窗回显用户ID 与原密码、四种非法输入各自给出正确中文提示、原密码错误被后端拒绝且弹窗不关、改成功后弹窗关闭 + 提示、本机凭据换成新密码、新密码能登录而旧密码失效、后台 /api/user-ids 列表里的密码已是新密码、刷新后自动登录仍能进系统、连续改密正常,并断言 0 异常 / 0 alert。共 24 项。用法 node run_password.js http://127.0.0.1:8794 ZZPWD1 666888 new8888。 |
_e2e/run_autologin.js | H5 登录页「自动登录」端到端验证(真 Chrome + 真鼠标事件):勾选后重新打开页面是否直接进入系统(全程不点任何按钮)、不勾选是否本机不留密码、退出登录后是否不再自动登录、本机密码失效时是否停在登录页并给出中文提示、失败是否会自动关掉开关,并全程断言 0 个 JS 异常 / 0 次 alert 打断。共 23 项。用法 node run_autologin.js http://127.0.0.1:8793 ZZAUTO1 666888。 |
_e2e/sfc_check.js | uni-app SFC 真实编译校验:用 HBuilderX 自带的 @vue/compiler-sfc 把 App.vue / login.vue / index.vue 的模板与 <script setup> 一起真编译(比只抽出脚本做 node --check 更严格:模板表达式、指令用法错误都会暴露)。另含 19 条静态回归守卫(崩溃防护 / 排序口径 / 上传压缩 / App 读图 / App 直传 / 全景视频保存 / 鸟瞰图下载 / 详情实时刷新,仅针对 index.vue;2026-10-02 新增「播放器底部有『⬇ 保存视频』入口」「保存视频走 uni.downloadFile,不用 App 端不支持的 getFileSystemManager」,本轮再加「鸟瞰图必须有『⬇️ 底图』入口」「底图下载必须打原图端点 /api/camp-map/download(不是页面那张压到 1600px 的显示图)」「收到 campsite 同步时要连『正开着的详情』里的全景图一起刷」)——防的是「App 端把 scroll-into-view / 选择器指向已销毁节点 → 视图层抛未捕获 getBoundingClientRect 空指针」这一编译期完全静默的坑:① #panoAnchor 标签上无 v-if;② showDetail() 复位 scrollTarget;③ closeDetail() 复位 scrollTarget;④ measureFsTools() 入口有 fsOpen 守卫;⑤ scheduleMeasureFsTools / cancelMeasureFsTools 成对且确有人调 cancel;⑥ 不存在裸 setTimeout(measureFsTools…);⑦ 考勤 shown 计算里降序必须走「整体倒序」arr.reverse();⑧ 排序比较器里不许再有内联的 dir 反向分支(同一天的 tie-break 必须跟着排序方向反向);⑨ App 端图片压缩必须带「缩尺寸」参数(plus.zip.compressImage 的 width:'…px');⑩ App 端压缩必须显式 format:'jpg'(PNG 源图 quality 无效);⑪ uni.compressImage 兜底必须带 compressedWidth;⑫ App 端读图必须走 plus.io(uni.getFileSystemManager() 在 js 引擎版 App 端不支持);⑬ 选图失败不得静默(uni.chooseImage 的 fail 不能是空函数);⑭ App 端二维码必须走 multipart 直传(uni.chooseMedia + uni.uploadFile → /upload/qr;改回「先读本地文件再 base64」就会重现「选完图没反应」)。用法 node sfc_check.js。 |
_e2e/guard_mutate.js | 守卫的变异测试:把上面 16 条守卫生效所对应的 bug 逐个「改回去」(v-if 锚点 / 漏复位 / 去掉 fsOpen 守卫 / 裸 setTimeout / 去掉 arr.reverse() / 比较器改回内联 dir 分支 / App 端去掉缩尺寸 width / format 改回 png / 去掉 compressedWidth / App 读图整段删掉 plus.io 分支 / 选图 fail 改回空函数 / App 端二维码整段删掉 multipart 直传 / 删掉播放器底部的「⬇ 保存视频」入口 / 保存视频改回 getFileSystemManager),断言对应守卫必须 FAIL —— 守卫永远通过就等于没有守卫。当前 19/19 全过(19 条守卫 → 18 个变异体,基准另算;本轮新增「删掉鸟瞰图『⬇️ 底图』入口」「底图下载改成打显示图 /api/camp-map/image」「onSync 里删掉刷新正开着详情那句」三个)。⚠️ index.vue 是 CRLF,跨行替换必须用 /\r?\n/,否则字面量 '\n' 会静默不命中、把「守卫没抓住」误判成通过;脚本末尾会单独打印「生效的变异体 N/M」来自证变异真的改动了源文件。用法 node guard_mutate.js(改过守卫正则后务必重跑)。 |
_e2e/pinlabel_mutate.js | 编号胶囊断言的变异测试:往页面里注入一段把 .cmap-pin .pc「改回旧值」的样式(字号 max(10px, 图钉×.32) / 内边距 2px 9px / 圆角 9px / 行高 1.35),再用与 run_pinratio.js 完全相同的判据量一遍 —— 必须 FAIL(实测字号 10 ≠ 6、胶囊 42×19.5 越界)。断言永远通过=没有断言,改过胶囊样式或判据后务必重跑。用法 node pinlabel_mutate.js http://127.0.0.1:端口(指向隔离实例)。 |
_e2e/flashno_mutate.js | 「新增置顶 2 秒显示该条固有编号」断言的变异测试:同一页面里先量「未变异」基线(断言应通过),再在页面内覆盖 noOf / setDateSeq(H5 覆盖 displayNoMap)成 2026-10-01 之前的旧实现(置顶期间一律用行位次 i+1、日期序列不排除新记录),同一断言必须 FAIL。用「日期落在中间」的新记录(id 999 → 固有编号 3、日期位次 2)保证两种口径数字不同、断言能真正区分。断言永远通过=没有断言,改过 noOf / setDateSeq 后务必重跑。用法 node flashno_mutate.js http://127.0.0.1:端口(指向隔离实例)。 |
_e2e/samedate_mutate.js | 「同一天多条记录 + 日期降序时编号列必须全程单调」断言的变异测试:同一页面里先量「未变异」基线(读出 12,11,…,1,断言应通过),再在页面内把 applySort(admin)/ sortAttList(H5)覆盖成 2026-10-01 之前的旧比较器(只把「日期」分支反向、「同一天」仍按固有编号升序、不整体倒序),同一断言必须 FAIL,且读出的正是用户截图那一串 7,8,9,10,11,12,6,5,4,3,2,1。数据形态=真实库抄回的 12 条(10-01 一天 6 条)。断言永远通过=没有断言,改过 applySort / sortAttList 后务必重跑。用法 node samedate_mutate.js http://127.0.0.1:端口(指向隔离实例)。 |
_e2e/_diag_h5drag.js | H5 全屏放大后「鼠标不能自由平移」的诊断脚本(定位原生拖放打断平移的问题)。 |
_e2e/run_camplist.js | 营位「三种显示模式(卡片 / 列表 / 鸟瞰图)」端到端验证:真鼠标点切换按钮,断言三个按钮存在、切换后互斥显隐与高亮正确、列表行数 = 当前筛选结果数、点行能开详情、搜索与状态卡联动、切回卡片 / 鸟瞰图正常、0 JS 异常。后台 20 项 + H5 12 项。用法 node run_camplist.js [http://127.0.0.1:9876]。 |
_e2e/shot_camplist.js | 营位三种模式的截图脚本:后台 / H5 各截「列表 / 卡片 / 鸟瞰图」三张,输出到 _shots/camplist_*.png,用于人工核对版式。 |
_e2e/perf_login.js | 登录后加载性能实测(本轮新增):用 CDP Network.emulateNetworkConditions 给 Chrome 真实限速(默认 150ms 延迟 / 250 KB/s 下行,模拟手机走 WiFi 或内网穿透),跑完整登录流程并量四件事:① 页面本身的传输字节与 DOMContentLoaded;② 登录后每个 /api/ 资源的传输字节(Resource Timing 的 transferSize,命中缓存是 0);③ 登录→功能对话框、进入考勤→表格出现、进入营位→卡片出现;④ 切鸟瞰图→底图可用(冷缓存与暖缓存各量一次)。每次跑都用全新 Chrome profile,否则上一轮的磁盘缓存会把传输量刷成 0。用法 node perf_login.js <baseURL> [用户ID] [密码] [标签]。 |
_e2e/run_mapslim.js | 鸟瞰图底图「瘦身 + 强缓存」端到端验证(本轮新增,共 19 项):① 接口契约(?slim=1 < 2KB 且不带 image、老接口仍带 image、图片端点是 image/* 且几百 KB、ETag 与 URL 里的 v= 一致、immutable 一年强缓存、If-None-Match 回 304);② 后台 / H5 / 游客页三端 DOM(都改用图片端点而非 data:base64、底图真的画出来、营位图标有渲染、0 JS 异常)。用法 node run_mapslim.js [http://127.0.0.1:9879]。 |
_e2e/run_qrperf.js | 营位电表二维码「独立图片端点」端到端验证:页面内实测各接口字节数(营位列表 /api/campsites 必须 < 200 KB、缩略图 < 80 KB)、断言列表已不再含 qr_code 整段 base64、卡片 img 走缩略图端点且 naturalWidth>0(真加载出图,不是 404 破图)、全屏预览走原图端点且分辨率明显大于缩略图、编辑回显可见、未改动时 campQrTouched=false(保存不会覆盖原图)、0 JS 异常,共 24 项。用法 node run_qrperf.js [http://127.0.0.1:9879]。 |
_e2e/run_qrup.js | 「二维码上传压缩 + 底图下载」的真实浏览器验证(整库副本 + 独立端口):后台与 H5 都拿一张原图级二维码跑一遍压缩函数(实测 0.88 MB → 129 KB、JPEG、最长边 1024),后台点「⬇️ 下载底图」真的落盘且字节数 = 原图,全程 0 个 JS 异常,共 20 项全过。 ⚠️ 基准由脚本自己种:启动时往副本库写一张 1800×2250 的照片级 PNG,并把存量收敛关掉( QR_IMG_LEGACY_BYTES = 1<<60)—— 否则 init_db() 一启动就把它压成 ~130KB,基准没了、压缩比断言失真。收敛本身由 run_qrfast.js 第 1 节专门验证。⚠️ 早先这里是「拿真实库里那张 1MB 老图当输入」——2026-10-02 后端上线存量收敛并重启后,真实库里的老图已被压回档位,这个前提就没有了,脚本会随真实数据时红时绿,所以改成自种基准。 |
_e2e/run_qrfast.js | 「H5 上传电表二维码提速 + 存量二维码收敛」的真浏览器端到端(整库副本 + 临时端口,真实数据零污染)34 项全过: ① 存量收敛:先在副本库里种一张「照片质感」的原图级 PNG( id=4),连同库里真实的两行(#1 1.36MB / #2 1.37MB)一起,断言启动后被逐条压小(≥9×)、最长边 ≤1024、都变成 JPEG、二次读取内容不变(幂等)。② 后端档位: /api/upload/qr 收到 902KB PNG,返回 100.6KB / 819×1024 的 JPEG(≥5×)。③ H5 快通道:6 个新函数就位、档位确为 1024/0.82;4000×3000 照片 6.78MB → 150KB(46×)。 ④ 回退路径: window.createImageBitmap 置为 undefined 后再跑,仍然产出 192KB / 1024 的合格结果(不能因为快通道不可用就摆烂),跑完确认已还原。⑤ 防退化守卫:喂一张 3MB 的「假图片」(解不开)→ 产物必须是 0 字节且弹出明确提示,绝不能把 4MB 的 base64 发出去。 ⑥ 真实上传路径:真 Chrome 里走 onQrSiteFile,6000×4500(15.20MB)→ 请求体 151KB(137×),入库后 113KB / 1024×768 JPEG。⑦ 模块图片: pickModImage 上传体积 6.79MB → 378KB(18.4×)且后缀已改 .jpg;0 个 JS 异常。⚠️ 页面里的输入用 DataTransfer 造真 FileList;上传体积靠临时包一层 window.fetch 求 FormData 各段字节和来量。 |
_e2e/run_panosync.js | 「H5 上传提速 + uni 实时刷新 + 后台详情页签保持」的真浏览器端到端(整库副本 + 临时端口 + 临时上传目录,真实数据零污染):① 后台打开营位「📷 全景图」时从外部(模拟手机端 / H5)上传一张 → 详情实时刷新且仍停在「📷 全景图」(不跳回「📜 入住记录」);② 切到「📜 入住记录」再上传 → 仍留在入住记录(证明是「记住当前页签」而不是写死 pano),切回全景图时按需重绘为最新条数;③ H5 上传前压缩:3600×2700 噪声图 9.47 MB → 请求体 1.36 MB(0.144×)、压后 1920×1440 且是 image/jpeg,而 640×480 的小图原样上传(请求体 = 文件字节数);④ 0 个 JS 异常,共 21 项全过。⚠️ 页面里用 DataTransfer 造 FileList 塞进真实 #campMediaFile,与真人选文件走同一条代码路径;上传体积靠临时包一层 window.fetch 求 FormData 各段字节和来量(Network.getRequestPostData 需要订阅 CDP 事件,这里用不着)。 |
_e2e/run_panofast.js | 「H5 上传全景图再提速」的真浏览器端到端(整库副本 + 临时端口 + 临时上传目录,真实数据零污染)27 项全过: ① 档位与后端对齐( PANO_UP_MAX===1920 && PANO_UP_Q===0.82);② 用「解码器调用计数」而不是计时来断言内核换了(计时在别的机器上必然抖):压缩 12MP 横图 / 竖图过程中 createImageBitmap 调用 0 次、FileReader 调用 0 次 —— 分别对应「不再按宽解一遍再按高解一遍」与「不经 base64」;③ 几何正确:竖图出来是 1440×1920(没被拉横)、比 1920 小的图保持原尺寸(不放大); ④ 真机路径请求体:12MP 原图 1.80MB → 272KB(0.151×)、27MP 大图 → 1920 边长 / 0.086×、640×480 小图原样上传; ⑤ 兜底链:把 URL.createObjectURL 打坏 → 仍能退回老路产出合格 JPEG(并断言确实走了 FileReader);⑥ 流水线:给「压缩段」和「POST 段」都打时间戳,断言两段区间真的重叠; ⑦ 两条反空转对照: compressImageFastBlob(老快路)在竖图上确实会调 createImageBitmap 两次;把「先压完再传」的串行版本跑一遍,区间确实不重叠(事件序列 comp,comp,post,comp,post,post vs comp,post,comp,post,comp,post);⑧ 提示文案(「正在上传 i/n …」/「已上传 N 张全景图」)、9 次上传全部入库、0 个 JS 异常。 |
_e2e/run_panodl.js | 全景图(照片 / 视频)下载的真实浏览器端到端(整库副本 + 独立端口 + 临时上传目录,真实数据零污染):后台登录 → 打开营位「全景图」页签 → 点每条的「⬇ 下载」真的落盘(文件名是后端给的中文名「营位<编号>_全景图_时间.jpg」、字节数与服务端完全一致)→ 照片灯箱「⬇ 下载原图」、全屏播放器「⬇ 下载」都能下 → 后端对不存在的媒体回 error 时不落盘、不导航 → H5 画廊同样能下(含 _cdFilename 解析后端中文文件名)→ 0 个 JS 异常,共 28 项全过。⚠️ 注释里记了两个坑:无头 Chrome 对同名文件是直接覆盖(有头版才会改成「名字 (1)」),所以每个阶段先清空下载目录;且 Chrome 默认只放行同源的一次自动下载,脚本会在启动前预写 Preferences 打开 automatic_downloads。 |
_e2e/shot_qr.js · _e2e/_probe_mapimg.js | 一次性排查脚本:二维码全屏截图 / 鸟瞰图底图端点探测(留档用)。 |
火石营地移动端/)| 文件(相对项目根目录) | 用意 |
|---|---|
火石营地移动端/index.html | H5 手机端界面(单文件,内联 CSS/JS):手机浏览器扫码或输入地址即用,含考勤、营位、鸟瞰图(只读 + 全屏查看)。与后台共用同一后端。 图片上传一律「前端先压」(手机原图动辄 3~8MB,转 base64 还要再大 1/3,原样直传就是几十秒)。两条压缩通道: ① 二进制上传通道(全景图、自定义模块图片)走 compressImageForUpload() → compressImageViaImg():<img> + URL.createObjectURL 只解码一次,用浏览器给好的 naturalWidth/Height(天然按 EXIF 摆正)一次性 drawImage 缩到目标画布,再异步 toBlob 出 JPEG —— 不放大(Math.min(1,…))、不变形、不经 base64、不用同步的 toDataURL。任何一步失败就退回老路 compressImageFile()(FileReader + toDataURL),两条都不行才用原文件。② 二维码通道(要交 data URL 给后端 qr_code)走 compressQrImage() → compressImageFastBlob():createImageBitmap(file,{resizeWidth}) 按目标尺寸解码,竖图再补一次 resizeHeight。档位:二维码 IMG_UP_MAX=1024 / IMG_UP_Q=0.82、全景图 PANO_UP_MAX=1920 / PANO_UP_Q=0.82(与后端 media_compress 的 PHOTO_MAX_SIDE / PHOTO_JPEG_QUALITY 完全对齐,不传后端马上要丢的字节)、自定义模块图片 MOD_UP_MAX=1400 / MOD_UP_Q=0.85;只压 >500KB 的非 GIF 图片(UP_COMPRESS_MIN),压后统一是 JPEG 所以文件名后缀同步改 .jpg。二维码另有一层防退化守卫:压不动就换更狠的档位(768 / 0.78)再救一次,仍不行就明确弹提示,绝不把原图级 base64 静默发出去。全景图多张上传走流水线: uploadCampMedia() 在 POST 第 i 张的同时就开始压第 i+1 张(压缩是纯 CPU、上传是纯等待,重叠起来批量总耗时接近减半),并显示「正在上传 i/n …」/「已上传 N 张全景图」。 |
火石营地uniAPP/)| 文件(相对项目根目录) | 用意 |
|---|---|
火石营地uniAPP/App.vue | 应用根组件:全局配置与样式、应用级生命周期。 |
火石营地uniAPP/main.js | uni-app 应用入口:创建并挂载 Vue 实例。 |
火石营地uniAPP/manifest.json | 应用配置:名称、图标、权限、各端(App / 小程序)打包参数。 |
火石营地uniAPP/pages.json | 页面路由与窗口样式(导航栏)配置。 |
火石营地uniAPP/vite.config.js | Vite 构建配置。 |
火石营地uniAPP/.hbuilderx/launch.json | HBuilderX 运行 / 调试配置。 |
火石营地uniAPP/common/api.js | 与后端交互的 API 封装:服务器地址解析、请求封装、WebSocket 实时同步。 |
火石营地uniAPP/pages/login/login.vue | 登录页(填写服务器地址 + 账号密码 / 超管密码)。 |
火石营地uniAPP/pages/index/index.vue | 主页面:考勤、营位、鸟瞰图等全部功能(逻辑与 H5 端一致)。 |
火石营地uniAPP/unpackage/dist/** | 编译产物(HBuilderX 自动生成,非源码):dev 为调试包、app-plus 为 App 运行包。可随时重新编译生成,通常不纳入交付。 |
说明:带 _ 前缀的脚本、_e2e/ 目录与 unpackage/dist/ 均为开发/构建产物,不影响系统运行;自动生成的 __pycache__/(Python 字节码缓存)等目录未单列。真正交付使用只需:app.py + frontend/ + 依赖清单 + 启动脚本(后端),以及 火石营地移动端/index.html(H5)与 火石营地uniAPP/ 源码(uni-app)。