======= 研发任务管理系统 H5 — 冷启动文件 ======= > 版本: 2.2 | 更新: 2026-09-06 | 用途: 研发任务管理系统(含库房集成)的冷启动/重建/排障手册 > ⚠️ **功能保全铁律(用户定稿):本系统功能只增不减,改版前先读本文件,改完逐项验证,功能不允许丢失。** > ⚠️ **测试铁律:测试必须用独立数据/独立进程,严禁污染生产数据(rd_tasks.json/accounts.json/store.db)。** ====== 一、系统概述 ====== **研发任务管理 + 研发库房管理 一体化系统**(钉钉生态): - **任务**:任务看板(跟踪任务/临时任务)、筛选(优先级/项目/状态/更新天数/搜索)、人员筛选、新增任务、更新记录(带时分+附件+可删)、子任务(进度联动)、工时统计、日报生成 - **账户体系**:管理员(武燕青)全权;普通用户(钉钉人员)只看自己任务、可编辑自己任务;管理员可配置用户权限 - **登录**:钉钉统一认证(JSAPI 免登 requestAuthCode / OAuth 授权码 / 工作台 authCode 三通道) - **库房集成**:底部导航 3 tab(任务/库房/我的);库房 = 研发库房管理(独立子系统,蓝图挂载) - **线上地址**:https://rd.huiflytech.com(Caddy HTTPS)= https://rd.huiflytech.com/rd/(任务)、/store/(库房) ====== 二、部署架构 ====== 用户/钉钉 → https://rd.huiflytech.com (Caddy :443/:80 自动 HTTPS) → rd-h5.service (systemd, Flask :13544, 统一进程) ├─ /rd/* → 任务前端 + API(rd_h5.py + rd_h5.html) ├─ /store/* → 库房前端 + API(rd_store.py 蓝图 + rd_store.html) └─ /api/* → 任务 API - systemd 服务:`rd-h5.service`(ExecStart=/home/ubuntu/projects/ctwing-2/venv/bin/python /home/ubuntu/.hermes/scripts/rd_h5.py,端口 13544) - Caddy:`/etc/caddy/Caddyfile`(rd.huiflytech.com → 127.0.0.1:13544;80→443 自动跳转;Let's Encrypt 自动证书) - 数据目录:`/home/ubuntu/docs/` ====== 三、数据文件 ====== | 文件 | 内容 | 说明 | |------|------|------| | `rd_tasks.json` | 任务数据(items/updated_at)| 主数据;normalize 兼容字段 | | `accounts.json` | 账户体系 | roles.admin(武燕青)/roles.user/perms(用户权限)/pwds | | `people.json` | 人员库 | 简称→全名(钉钉通讯录);**注意:无 roles 字段**(admin 判定在 accounts.json)| | `ding_org.json` | 钉钉通讯录全量 | MCP 同步缓存(17 人:姓名/userid/部门);/api/ding/org 优先读此文件 | | `leave.json` | 钉钉请假记录 | MCP OA 同步(cron 0210e63abf25 工作日 08:30)| | `tags.json` | 标签库 | 任务标签(默认/周报汇报 + 自定义)| | `store.db` | 库房 SQLite | samples/requests/history 表(软删除 deleted 标记)| | `.env` | 凭证 | DINGTALK_CLIENT_ID/SECRET(慧飞任务应用 dingh2ogz...)、DINGTALK_CORP_ID=4901944860、HALO_DB_PASSWORD 等 | ====== 四、核心功能 ====== ===== 1. 任务看板 ===== - 任务类型:跟踪任务([优先级](项目)内容)/ 临时小任务(⏱️ 当天) - 筛选:优先级/项目/状态/未更新天数(工作日)/搜索;人员标签筛选(点击) - 人员标签:显示所有有任务人(从任务提取);"+ 更多"打开钉钉通讯录全量(17 人) - 任务详情:状态/进度/负责人/子任务(进度联动)/更新记录(带时分+附件)/总耗时 ===== 2. 账户体系(2026-08-31) ===== - 管理员:武燕青(全部权限:查看/编辑/删除所有任务、账号管理、生成日报、新增) - 普通用户:只见自己任务(负责人=自己或子任务负责人=自己,简称/全名变体匹配 is_mine)、可编辑/删除自己任务(can_edit/can_delete) - 权限配置:管理员「账号管理」弹窗(view_own_only/edit_own/delete_own/manage_subtask 开关 + 设置口令) - 后端强制校验:can_edit/can_delete 在 update_task/add_note/del_note/del_task/subtask_note 接口执行(403) - ⚠️ isAdmin 判定:统一账户体系(USER_ROLE=admin 或 名字=武燕青);**不依赖 people.json roles(空)** ===== 3. 钉钉统一认证(3 通道) ===== - **JSAPI 免登**(主,钉钉内):UA 检测 DingTalk → 加载 dingtalk.open.js → requestAuthCode({corpId: ding3049357e0fcfed24f5bf40eda33b7ba0}) → code → /api/ding/login(gettoken+getuserinfo)→ 自动登录 - **OAuth 授权码**(浏览器):/api/ding/oauth/url 生成授权 URL → 回跳 /rd/ding_cb?code → /api/ding/oauth/cb(userAccessToken+users/me)→ 登录 - **工作台 authCode**:URL 带 authCode → 直接 dingLogin(失败自动降级 JSAPI) - 登录态:localStorage rd_user(同域任务/库房共享);登录用户自动退出只读模式(exitReadonly) ===== 4. 周报生成(2026-08-31) ===== - 📊 生成周报按钮(btnWeek,管理员):/api/report/week 生成**上一周**研发工作周报 - 三部分:①研发项目推进(各项目在管/更新/新增/完成+主要进展总结式)②研发管理推进(任务系统/库房系统运行状态)③研发任务日报(正常推进) - 弹窗显示 + 📋 复制按钮;⚠️ 弹窗打开必须 classList.add('open')(style.display 会导致关闭按钮失效) ===== 4.x 统一项目列表:任务 → 测试 / 库房 / 评审会(2026-09-17) ===== **需求**:研发任务系统管理员可**手动添加项目**;项目列表**同步**到测试系统、库房系统、评审会三个子模块。 **① 数据源(唯一)** - 文件:`/home/ubuntu/docs/projects.json`(数组,首次自动生成默认 13 项) - 默认:拓远T1 / 普惠P1 / 金穗S1 / LoRa网关 / P3 / P5 / 流量计 / 表压 / 软管泵 / 施肥阀 / 施肥机 / 生产订单 / 其他 **② 后端接口**(`rd_h5.py`) | 方法 | 路径 | 权限 | 说明 | |---|---|---|---| | GET | `/api/projects` | 全员 | 返回 `{ok, projects:[...]}` | | POST | `/api/projects` | **仅管理员**(`is_admin`)| `{action:"add"/"del"/"rename"/"move", name, old?, dir?}` | - `add` 重名拦截 400;`del` 不存在 400;`rename` 校验新旧名;`move` 上移/下移排序 - 关键函数:`load_projects()` / `save_projects(lst)` / `api_projects()` **③ 四处接入** | 系统 | 文件 | 接入方式 | |---|---|---| | 研发任务 | `rd_h5.html` | `loadProjects()` 启动时拉取覆盖 `PROJ_LIST`;管理员见顶部「⚙️ 项目管理」按钮 → `projManage()` 面板(列表 + 添加 + 删除 + 改名 + ▲▼排序);`projRefreshUI()` → 调用 `load()` 刷新 | | 评审会 | `review.html` | `PROJECTS` 全局 + 启动 `loadProjects()`;`renderEdit()` 的项目下拉改用 `PROJECTS`(当前值不在列表时自动补入)| | 库房 | `rd_store.html` | `META.projects` 合并统一列表;`hisProj` datalist 与顶部「项目(全部)」筛选下拉同步刷新 | | 测试 | `test.html` | 物料编码输入框加 `list="projDL"` + `loadProjects()` 填充项目名提示(2 处输入框)| **④ 项目筛选框支持"直接输入新建"(2026-09-17 追加)** - UI:``**(可下拉选、可直接打字);右侧出现「+ 新建项目」链接 - `renderProj()` 改为填充 ``;`applyFilter()` 读 `fProj.value` 兼容不变 - 新增 `projFilterHint()`:输入值不在项目列表且当前用户是管理员 → 显示「+ 新建项目」 - 新增 `projCreateFromFilter()`:调用 `POST /api/projects {action:add}` → 成功后刷新 `PROJ_LIST`/datalist/筛选;**保存即同步到 测试/库房/评审会** - 非管理员输入新名称 → 不显示新建按钮(接口也会 403 拦截) - 页面结构验证:`fProj` input + `projDL` datalist + `projCreateFromFilter()` 均已上线 ✅ - 接口验证:新建成功 ✅ → 列表末尾出现 ✅ → 三系统页面均含 `/api/projects` 调用 ✅ → 删除清理无残留 ✅ **⑤ 项目清单更新(2026-09-17)** 新增 4 个项目:**普惠P3 | 普惠P5 | 云枢AV1 | 云联LR1**(现共 18 项) | # | 项目 | |---|---| | 1-6 | 拓远T1 | 普惠P1 | 金穗S1 | LoRa网关 | P3 | P5 | | 7-12 | 流量计 | 表压 | 软管泵 | 施肥阀 | 施肥机 | 生产订单 | | 13-16 | 其他 | M10面板端连接器 | **普惠P3** | **普惠P5** | | 17-18 | **云枢AV1** | **云联LR1** | **四系统同步验证** - 统一接口 `/api/projects` → 18 项,4 个新项目全部在列 ✅ - **评审会**(`/review/`)→ 新建会议的项目下拉 18 项含新增 4 项 ✅(浏览器实测) - **库房**(`/store/`)→ 项目筛选下拉 18 项含新增 4 项 ✅(浏览器实测) - **测试**(`/test/`)→ 物料编码输入框项目提示 18 项含新增 4 项 ✅(浏览器实测) - **研发任务**(`/rd/`)→ 页面含 3 处 `/api/projects` 调用,机制相同(钉钉环境下浏览器自动化受限,未截图验证) > ⚠️ 测试系统踩坑:`projDL`(datalist)随**表单渲染**才存在,页面加载时填充无效 → 修复:`fillProjDL()` 在 `renderEdit()`/`openItem()` 时调用 + 输入框加 `onfocus="fillProjDL()"` + 加载后重试 3 次。 **⑥ 验证(接口级,全部通过)** - GET 返回 13 项 ✅ - 普通用户 POST → **403**(仅管理员可维护项目)✅ - 管理员 add → 成功 ✅|重复 add → 400 拦截 ✅|rename ✅|move(上移)✅|del ✅ - 清理后无残留 ✅ - 4 个页面 200:`/rd/` `/review/` `/store/` `/test/` ✅ > ⚠️ 浏览器自动化验证受限:`/rd/` 在钉钉环境下会跳转登录页,无法稳定取到页面内全局函数(已知限制),UI 已由代码审查 + 接口验证覆盖。 ====== 4.64 修复:新建项目弹窗被底部导航遮挡(2026-10-10) ====== **问题**:研发任务系统「+ 新建项目 / ⚙️ 项目管理」弹窗底部被**底部导航栏挡住**,看不见也点不到「+ 添加项目」「关闭」。 **根因(z-index 层叠)** | 元素 | 原 z-index | 说明 | |---|---|---| | `.modal`(弹窗遮罩)| **100** | ❌ 低于导航栏 | | `.tabbar`(底部导航)| **999** | 盖住了弹窗 | **修复(`rd_h5.html`)** .modal{ ... z-index:1100; padding:20px 20px 78px } /* 100→1100;底部留白避开导航 */ .mbox { ... max-height:calc(100vh - 140px) } /* 88vh→按视口扣除,不越过导航栏 */ **排查其他页面(确认无同类问题)** | 页面 | tabbar z-index | 弹窗 z-index | 结果 | |---|---|---|---| | rd_h5.html | **999** | 100 → **1100** | ✅ 已修 | | test.html | 50 | 9999 / 99999 | ✅ 正常 | | review.html | 50 | 9999 | ✅ 正常 | | family.html | — | 9999(.mask 99)| ✅ 正常 | **验证**:服务端与页面均已生效(`.modal{z-index:1100}`、`.mbox{max-height:calc(100vh - 140px)}`);弹窗 z-index(1100) > 导航(999) → 弹窗必在导航之上;内容框高度扣除 140px → 底部按钮(+添加项目 / 关闭)不再被遮挡。 ====== 4.65 修复:提示条 toast 被底部导航遮挡(2026-10-10) ====== **问题**(用户截图):页面底部弹的深色圆角提示条**只露出上半截**,下半截被白色底部导航栏吃掉。 **根因(层叠 + 定位双重问题)** | 属性 | 原值 | 问题 | |---|---|---| | `.toast` z-index | **200** | ❌ 低于 `.tabbar` 的 **999** → 被压在导航栏之下 | | `.toast` bottom | **24px** | ❌ 正好落在导航栏占据的区域内(导航栏高约 33~53px)| 两个问题叠加 → 重叠部分被不透明导航栏盖住,看起来就是"只露上半截"。 **修复(`rd_h5.html`)** .toast{ ... bottom:86px; ... z-index:1200 } /* 24→86px;200→1200 */ - bottom 86px > 导航栏高度(约 33~53px)→ 提示条整体位于导航栏之上 - z-index 1200 > 999 → 即使重叠也显示在导航之上 **实测验证(浏览器真实渲染)** | 项 | 数值 | |---|---| | 视口高 / 导航栏高 | 577px / 33px(测试页同款导航;研发页导航含 20px 图标更高,约 53px)| | 提示条高度 | 39px | | **修复后**(bottom 86px)| 提示条底边距视口 **86px**,与导航栏顶边**零重叠** → 完整可见 ✅ | | **修复前**(bottom 24px)| 底边 y=553,导航顶边 y=544 → **被覆盖 9px**(研发页导航更高 → 覆盖约 29px,接近整条)❌ | | 截图对比 | 上方(新值)完整;下方(旧值)被导航截断 —— 与用户截图一致 ✅ | **其他页面排查**:test/review/family 的 toast 未发现同类问题(其 tabbar z-index 为 50/无)。 ====== 4.66 修复:项目下拉缺新增项目(银穗Y1 等)(2026-10-10) ====== **问题**:项目「银穗Y1」在项目管理面板显示"已添加",但下拉列表里看不到(同理 M10面板端连接器 / 普惠P3 / 普惠P5 / 云枢AV1 / 云联LR1 也会消失)。 **排查** | 检查项 | 结果 | |---|---| | `projects.json` | ✅ 已有「银穗Y1」(共 19 项)| | `GET /api/projects` | ✅ 返回含「银穗Y1」| | `/rd/` 缓存头 | ✅ no-store(不是缓存问题)| | **`loadProjects()` 调用点** | ❌ **只在 `addOwnerMore()` 里被调用;页面初始化流程从未调用** | **根因**:`PROJ_LIST` 的默认值是**硬编码的 13 项**(不含 M10/普惠P3/P5/云枢AV1/云联LR1/银穗Y1),页面每次加载都不会去拉 `/api/projects` → **刷新后下拉退回旧列表**,新增项目消失(管理面板里因为一次操作刷新过 PROJ_LIST 所以显示"已添加",两者不一致)。 **修复(`rd_h5.html`)** function load(){ loadProjects(true); // ★ 2026-10-10:初始化即拉取统一项目列表 ... } function loadProjects(noRefresh){ // 增加参数避免初始化时的 load() 递归 fetch('/api/projects').then(...).then(function(d){ if(d&&d.ok&&d.projects&&d.projects.length){ PROJ_LIST=d.projects; renderProj(); // 刷新筛选框下拉(projDL) refreshProjSelects(); // 刷新已打开表单的项目下拉(#aProj / #mProj,保留已选值) if(!noRefresh){ projRefreshUI(); } } }); } function refreshProjSelects(){ ... } // 新增 **行为验证(Node 里跑 rd_h5.html 的真实函数 + stub DOM/fetch)** | 场景 | 结果 | |---|---| | 修复前(硬编码默认列表)| 下拉 **13 项**,含「银穗Y1」:**❌ 没有** ← 复现用户问题 | | 修复后(初始化调 `loadProjects(true)`)| PROJ_LIST = **19 项**(末尾 云枢AV1, 云联LR1, **银穗Y1**)| | 筛选框 `projDL` | ✅ 含「银穗Y1」(19 项)| | 新增任务表单 `#aProj` | ✅ 含「银穗Y1」| | 编辑任务表单 `#mProj` | ✅ 含「银穗Y1」| **同类排查**:`test.html`(初始化顶层调用 `loadProjects()` ✅)、`review.html`(line 137 初始化调用 ✅)、`rd_store.py`(不使用该下拉)→ 仅 `rd_h5.html` 有此问题。 ====== 4.67 修复:添加附件后"无法保存任务修改"(2026-10-16) ====== **现象**(用户报):在任务详情里上传附件后点保存,任务修改没有保存 / 附件不见了。 **排查**:后端全部正常(`/api/upload` 200、`POST /api/tasks//note` 200、`POST /api/tasks/` 200)。用浏览器真实复现后定位到**两个前端缺陷**: ===== 缺陷① 更新内容留空时,附件与耗时被静默丢弃(主因) ===== `saveTask()` 里判断是 `if(noteText){ ...POST note(带附件)... } else { 只保存任务字段 }` —— **只有填了"更新"文字才会记录 note**,因此"只加附件 / 只填耗时"时: - 附件已上传成功(UI 也显示了文件名)→ 但**不落库** ❌ - 耗时也不记录 ❌ 用户截图正是这个状态:`更新 [空] | 耗时(时) [空] | 添加文件 📎 | 📎 YQ销售产品…` ===== 缺陷② 上传中点保存会丢附件 ===== 上传是异步的,`mAttUrl` 还没赋值就点保存 → 附件同样丢失。 **修复(`rd_h5.html`)** var mAttNameCur=''; // 附件原始文件名(用于默认文案) var mAttPending=null; // 上传中的 Promise function waitAtt(cb){ if(!mAttPending) return false; toast('附件上传中,完成后自动保存…'); mAttPending.then(cb); return true; } // saveTask:① 上传中先等待 ② 只要 文本/附件/耗时 任一有值就记录 if(waitAtt(function(){ saveTask(id); }))return; var hasExtra=!!(mAttUrl||(!isNaN(dur2)&&dur2>0)); if(noteText||hasExtra){ var nb={text: noteText||(mAttUrl?('📎 附件:'+(mAttNameCur||'附件')):'(记录耗时)')}; ... } - `uploadAtt()`:上传中显示「📎 文件名(上传中…)」、成功记录 `mAttUrl+mAttNameCur`、失败给 toast、用 `.catch` 兜底 - `markDone()`(标记完成)同步应用相同的等待与判定 **浏览器实测(真实页面 + 真实文件上传)** | 场景 | 修复前 | 修复后 | |---|---|---| | 填了更新内容 + 附件 → 保存 | ✅ 正常 | ✅ 正常(未回归)| | **只加附件、更新留空 → 保存** | ❌ 无 `/note` 请求,附件丢失 | ✅ `/note` 200,附件落库,记录文案「📎 附件:文件名」| | **上传中点保存** | ❌ 附件丢失 | ✅ 提示"附件上传中,完成后自动保存…",随后自动补存 ✅ | | 只填耗时不填文字 | ❌ 耗时丢失 | ✅ 记录「(记录耗时)」+ 耗时 ✅ | **清理**:验证用的临时任务与测试附件已删除(任务数回到 106);用户自己上传的 3 个 xlsx 保留未动。 ====== 4.68 评审会:新增「会议现场照片」+ 去掉「记录员」(2026-10-22) ====== ===== ① 会议现场照片 ===== **数据**:`meetings` 表新增 `photos TEXT DEFAULT ''`(多张用 `;` 分隔 URL),`migrate()` 自动加列。 **接口**:`POST /api/review/meeting/` 的 UPDATE 增加 `photos=?`(沿用旧值兜底);GET 用 `SELECT *` 自动带出。 上传复用已有 `POST /api/review/upload` → 返回 `/review/attachments/`。 **前端**(`review.html` 会议编辑表单,签名行下方): 会议现场照片 📷 拍照 🖼 手机图库 [缩略图 74×74] × N ← 点图可放大,右上角 ✕ 单张删除 - `upMeetPhoto()` 上传后写入 `CUR.photos` 并即时刷新缩略图(提示"记得保存") - `delMeetPhoto()` 从列表移除 - `collectForm()` 带上 `photos` → 新建/保存/生成报告三条路径都会提交 ===== ② 去掉「记录员」 ===== - 要点记录表单里 **删除「记录人员」输入行**(原 `p_user` + 👥 选择器) - **数据库 `points.p_user` 字段保留**,保存时沿用旧值(`collectPoints()` 里若无输入框则取 `p.p_user`)→ **历史数据不丢** - 清理无用的 `pickPointUser()` 选择器;把共用的选择辅助函数改名 **`pickIntoBtn()`** 供「记录负责人」使用(重要:原 `pickOwner` 依赖它,改名时必须同步) - 保留:记录时间 / 记录内容 / 记录负责人 / 记录图片 / 附件 / 状态 ===== 验证 ===== | 项 | 结果 | |---|---| | `photos` 列迁移 | ✅ 自动加列成功 | | 上传 2 张 → 保存 → 回读 | ✅ `photos` 两个 URL、顺序正确、文件真实落盘 | | 要点 `p_user` 历史值 | ✅ 保留(前端不再显示)| | 浏览器表单 | ✅ 含「会议现场照片」+ 拍照/图库入口 + `#meetPhotos` 容器 | | 要点区 | ✅ 记录时间/内容/负责人/图片/附件正常,**「记录人员」已消失** | | JS 报错 | ✅ 无 | | 真实数据 | ✅ 14 个真实会议完好;临时验证会议与测试图片已清理 | ⚠️ **踩坑记录**:改 UPDATE 语句时先加了 `photos=?` 占位符但参数没同步插入 → `sqlite3.ProgrammingError: 17 placeholders vs 16 parameters` → 保存 500。 **教训(已在纪律中)**:改 SQL 必须核对**占位符数量 = 参数个数**,改完立即用临时数据跑一次保存。 ====== 4.69 修复:评审会「无法导出报告」(2026-10-22) ====== **现象**:评审会点「网页报告 / Word版 / PDF版」没有反应,导不出来。 **排查**:服务端三种格式都正常 —— `GET /api/review/report/3` → 200(4091 字节 HTML);`?fmt=docx` → 200(2.5 MB,Word 2007+);`?fmt=pdf` → 200(143 KB,6 页)。**问题纯在前端**。 **根因(两个)** 1. **弹窗被拦截**:导出流程是 `fetch(保存) → .then(() => window.open(url))`。 `window.open` 写在**异步回调**里,不再算用户手势 → 微信/Chrome/Safari 一律当弹窗拦截 → **点了完全没反应**(最常见原因)。 2. **失败无提示**:保存失败时 `.then(d => { if(d.ok) window.open(...) })` 没有 else → **什么提示都没有**,用户只能看到"点了没用"。 **修复(`review.html`)** function _reportFlow(fmt){ // 点击瞬间同步开占位窗口(同步调用不算弹窗) var w = window.open('about:blank','_blank'); fetch(保存会议).then(d => { if(!d.ok){ w.close(); toast('导出失败:'+d.error); return; } // ← 明确报错 w.location.href = url; // 让占位窗口跳转 // 开不出窗(微信)→ location.href 当前页兜底 }).catch(() => { w.close(); toast('导出失败(网络错误),请重试'); }); } function genReport(){ _reportFlow(''); } function genExport(fmt){ _reportFlow(fmt); } // docx / pdf - 微信环境导出 Word/PDF 会额外提示:**「若打不开请点右上角「⋯」→「在浏览器打开」」** - 网页报告 / Word / PDF 三条路径统一走 `_reportFlow()` **验证** | 项 | 结果 | |---|---| | 服务端 Web 报告 | ✅ 200,4091 字节 HTML | | 服务端 Word | ✅ 200,2.5 MB,`Microsoft Word 2007+` | | 服务端 PDF | ✅ 200,143 KB,6 页 | | 前端三种格式 | ✅ 跳转 URL 正确(`/report/`、`?fmt=docx`、`?fmt=pdf`)| | 失败路径(会议不存在)| ✅ toast「导出失败:不存在」+ 占位窗口关闭 + **不误跳转** | | JS 错误 | ✅ 无 | | 真实数据 | ✅ 14 个真实会议完好;临时验证会议已删 | ⚠️ **同源教训**:凡"点击 → 异步请求 → 再打开窗口"的流程,`window.open` 必须**在点击事件内同步调用**,否则必被拦截。 ====== 4.70 评审会:去签名 + 修「照片没保存/导出不能用」根因(2026-10-22) ====== 用户反馈三症状:**上传的现场照片没保存住**、**三种报告都导不出**、签名功能要去掉。 ===== ★ 根因:`/review/` 页面没有禁缓存头(一处修好,前两个症状全解决) ===== 修复前 /review/ → cache-control: no-cache + ETag + Last-Modified 修复后 /review/ → cache-control: no-store, no-cache, must-revalidate, max-age=0 pragma: no-cache | expires: 0 (与 /test/ 一致) `no-cache` 只是"可缓存但需校验",配合 ETag 微信会长期复用旧 HTML → 用户手里一直是**没有「会议现场照片」字段、导出还是老代码**的旧页面。 所以:照片"没保存住"(旧页面根本没这个字段)+"导出不能用"(旧 window.open 逻辑)= 同一个根因。 ===== ① 去掉手写签名 ===== - 表单去掉「与会人员签名」行;`collectForm()` 不再提交 `signatures` - Web / Word / PDF 报告去掉 `_sig_block()` / `_sig_docx()` 签名块 - **数据库 `meetings.signatures` 字段保留**(历史数据不删,不再显示) ===== ② 导出加固 ===== - **微信内不再 `window.open`**(必被拦截)→ 保存成功后当前页 `location.href` 跳转 - 非微信:点击时同步开占位窗口,保存成功后让该窗口跳转(避免异步弹窗被拦) - 保存失败 → 明确 toast「导出失败:原因」并关闭占位窗口 ===== 完整实测(临时会议,全流程真实操作) ===== | 步骤 | 结果 | |---|---| | 表单含「会议现场照片」 | ✅ | | 签名行已消失 | ✅ | | 真实 file input 上传照片 | ✅ `CUR.photos` 有值、缩略图渲染 1 张 | | 点保存 | ✅ toast「✅ 已保存」,5 个写请求 0 错误 | | 回读数据库 | ✅ **photos 已落库**、议题/概述/意见均已保存 | | 三种报告 | ✅ Web 1854B / Word 35KB / PDF 38KB,全部 200 | | 真实会议 #3(库里有旧签名)| ✅ 三格式 200,报告页面「签名」出现 **0 次** | | JS 错误 | ✅ 无 | | 收尾 | 临时会议 #28 + 测试图片已删除,14 个真实会议完好 | ⚠️ **教训**:新增功能后如果用户"看不到/用不了",**先查页面缓存头**(`curl -sI` 看 cache-control)。 `/test/` `/rd/` `/store/` `/family/` `/extracur/` 都有 no-store,唯独 `/review/` 漏了 → 已补齐。 ====== 4.71 评审会导出:彻底去掉弹窗(修 Windows「获取打开此'about'链接的应用」)(2026-10-22) ====== **现象**(用户截图):**电脑**上点「网页报告/Word版/PDF版」→ Windows 弹出 > 获取打开此'about'链接的应用 —— 你的电脑没有可打开此链接的应用。请尝试在 Microsoft Store 中查找兼容应用。 **原因**:4.69 为了绕开"异步弹窗被拦",改成点击时先 `window.open('about:blank')` 占位。 `about:blank` 在部分 Windows 浏览器里会被当外部协议交给系统处理 → 弹上述对话框 ❌ **定稿方案:完全不用弹窗** // 保存会议 → 成功后当前页跳转 fetch(保存).then(d => { if(!d.ok){toast('导出失败:'+d.error);return;} location.href = url; }); | 格式 | 行为 | |---|---| | 网页报告(text/html)| 当前页打开报告,按返回键回编辑页 | | Word/PDF(`Content-Disposition: attachment`)| **只触发下载,页面不跳走** | - 微信环境保留提示:导 Word/PDF 若无下载 →「点右上角「⋯」→「在浏览器打开」」 - 缩略图的 `window.open(this.src)`(看大图)是 http 地址,不受影响,保留 **实测** | 项 | 结果 | |---|---| | 网页报告 | ✅ `window.open` 调用 **0 次**,页面成功跳到 `/api/review/report/29`,标题「…评审报告」正确 | | Word | ✅ `window.open` 0 次、下载触发、页面停留原地址 | | PDF | ✅ 同上 | | 失败路径 | ✅ toast「导出失败:原因」| | JS 错误 | ✅ 无 | ⚠️ **教训**:不要再为了"防弹窗拦截"用 `window.open('about:blank')` 占位 —— Windows 会当外部链接处理。**同页跳转(`location.href`)+ 服务端 attachment 头** 是跨平台最稳的做法。 ====== 4.72 ★评审会「四个按钮全部失效」根因(我引入的 bug)+ 三处加固(2026-10-23) ====== **现象**:会议里 💾保存 / 📄网页报告 / 📝Word版 / 📕PDF版 **全部点了没反应**; 手机端更是「没有任何提示,就是不生成报告」。 **★ 真根因(自查出的自造 bug)** 4.70 去掉「记录人员」输入框时,`collectPoints()` 里写成了: p_user:(el.querySelector('.p_user')?el.querySelector('.p_user').value:(p.p_user||'')) // ↑ 这个 p 在 forEach(function(el){}) 里根本不存在 → 点任一按钮 → `Uncaught ReferenceError: p is not defined` → **连 fetch 都没发出、toast 也没弹** → 四个按钮静默全废(保存/三种报告都走 collectPoints)。 **修复** document.querySelectorAll('#pointsBox .point').forEach(function(el,i){ var _pu=(CUR_POINTS[i]&&CUR_POINTS[i].p_user)||''; // 按索引沿用库里的记录人员,数据不丢 ... }); **为什么上一轮测试没抓到**:测试用的是**空会议**(要点 0 条)→ `forEach` 根本没执行 → 没报错。 教训:**测保存/导出必须用"有现存要点记录"的会议**(本次用「继承真实会议 #3」的临时会议复现,3 条要点)。 **顺带修的三处** 1. `new_meeting`(继承会议)500:`p.get("p_status")` → `sqlite3.Row` 没有 `.get` → 改 `p["p_status"] or "未解决"` 2. 页面顶部显示版本号 **v2026-10-23** → 一眼确认手机是否加载新版本 3. 新增**全局 error 监听** → JS 报错时明确 toast「⚠️ 页面脚本错误:…」,不再静默失效 **实测(继承真实会议 #3 的临时会议,3 条真实要点,真点击按钮)** | 按钮 | 结果 | |---|---| | 💾 保存 | ✅ toast「✅ 已保存」,`POST /api/review/meeting/30` → 200 | | 📝 Word版 | ✅ toast「📝 正在生成 Word…」+ 200,页面停留(下载)| | 📕 PDF版 | ✅ toast「📕 正在生成 PDF…」+ 200,页面停留 | | 📄 网页报告 | ✅ 当前页跳到 `/api/review/report/30`,标题「…评审报告」正确 | | JS 错误 | ✅ 0 个 | | 数据 | ✅ 3 条要点内容与库内记录人员全部保留 | | 真实会议 #3(只读检查)| ✅ 版本标记 v2026-10-23、collectPoints/collectForm 均正常、无报错 | ⚠️ **手机端 Word/PDF 说明**:微信内置浏览器**不支持下载 docx/pdf**(这是微信限制,不是系统 bug)。 请在微信里点右上角「⋯」→「在浏览器打开」,再导出即可;网页报告在微信用可以直接看。 ====== 5. 钉钉请假同步(2026-08-31) ====== - leave.json(date/name/type/start/end/status);/api/leave?date= 查询 - 人员筛选时名字后显示当天请假标记;任务行负责人标签后同样显示 - **日报:负责人姓名后直接显示(X 请假)**;每日 08:30 cron 同步(请假模板 PROC-4DD795D7-E54A-4942-B91A-6743ED78AD42) ===== 6. 任务标签(2026-09-02) ===== - 标签库 tags.json(预置:默认/周报汇报)+ /api/tags(GET/POST 自定义新增) - 任务详情 🏷️ 区:标签库多选 + 自定义(自动入库);新任务默认 ["默认"] - 任务行/详情标题显示非默认标签(🏷️橙色高亮),"默认"不显示在行上 ===== 7. 评审会模块(2026-09-05) ===== - 独立蓝图 review_api.py + 页面 /review/(底部导航 5 tab:任务/库房/测试/评审会/我的) - 会议:新建/继承(复制全部内容+议题自动续序号)/项目/开始时间/预计时长/结束时间(自动算实际)/会议人员(钉钉多选)/负责人/**评审阶段**/标签 - ★ 评审阶段(2026-09-16 更新,共 8 个):**需求分析/可行性评估** → 立项评审 → 方案设计 → 详细设计 → 样机试制 → 测试验证 → 量产导入 → 结项评审 - 定义位置:`review.html` 的 `stageSel`(唯一来源,改这里即可) - ★ **与会人员手写签名**(2026-09-16 新增): - 前端:编辑页「与会人员签名」区 → 与会人员一人一张卡片(未签名/已签缩略图)→ 点「✍️ 签名」弹出手写板(canvas 900×360,**支持手指触摸与鼠标**,可清除重写) - 存储:`meetings.signatures`(TEXT,JSON `{人名: "data:image/png;base64,..."}`);保存接口**仅当传入非空才更新**(防前端未带字段时清空) - 人员来源:`meetings.members`(分号/逗号分隔)解析 - 报告:Web 内嵌 ``;Word/PDF 用 `add_picture` 插入真实签名图(原为"打印后手写签字"的空白表) - 相关函数:`sigLoad/renderSigns/openSign/initPad/clearPad/savePad/clearSign/closeSign`;后端 `_parse_sig/_sig_one/_sig_block/_sig_docx` - ★ **自动记录签名时间**(2026-09-16):签名为 `{人名: {"img": "data:image/png;base64,...", "time": "YYYY-MM-DD HH:MM"}}`(**兼容旧格式纯 base64 字符串**,无时间不报错);前端签名卡片显示 `🕐 时间`;报告 Web/Word/PDF 均在签名下方显示时间 - ★ **签名后自动提交**:`savePad` 完成后自动调用 `saveMeeting()` 落库(避免忘记点保存导致报告无签名) - 实测:3 人签名(2 新格式含时间 + 1 旧格式)→ Web 报告 3 张签名图 + 时间正确显示 ✅|Word 内嵌 3 张图 + 含时间 ✅|PDF 69KB ✅ - ★ **报告要点表按评审阶段动态列**(2026-09-16): - 判定:`_pt_need(s)` = 阶段含「需求分析」;`_pt_cols(s)` 返回列定义 - **需求分析/可行性评估阶段** → 精简为 4 列:**序号 | 记录内容 | 日期 | 负责人**(去掉 状态/优化方案/记录图片/附件) - 其他阶段 → 保持 8 列(序号/记录内容/状态/优化方案/记录图片/附件/日期/负责人) - 生效范围:Web 报告 + `_export_docx` + `_export_pdf`(共 3 处) - 实测:需求分析阶段 Web/Word 表头均为 4 列 ✅;立项评审阶段对照仍为 8 列 ✅ - ★ **签名合并机制(2026-09-17 修复覆盖丢失 bug)**: - 现象:#9 会议中超哥(席江锋)的手写签名消失(DB 仅剩武燕青/刘瑞红) - 根因:前端 `SIG` 是打开页面时的快照 → 保存时**整体覆盖** `signatures` 字段;多人用各自设备签名时,后保存者会把先前他人的签名冲掉 - 修复:后端改为**按人名合并**(`_merged = dict(old); _merged.update(new)`);传入 `{"人名": null}` 表示删除该人签名 - 前端:新增 `SIG_DEL` 删除标记数组;`saveMeeting` 提交 `{本机已有签名…, 已清除人名: null}` - 实测(模拟三设备并发):甲签 → {甲};乙用**旧快照**签 → **{甲,乙} 未丢失** ✅;丙签 → {甲,乙,丙};清除乙 → {甲,丙} ✅ - ⚠️ 已丢失的签名**无法恢复**(无备份)→ 需当事人重新签名 - 实测:签名 PNG 落库 ✅|Web 报告含签名图 ✅|Word 内嵌 `word/media/image1.png` + drawing 引用 ✅|PDF 52KB ✅ - 要点记录:序号/时间/记录人员(钉钉)/内容/优化方案/记录负责人(钉钉)/状态(☐未解决☑已解决复选框同行)/拍照+图库双传/附件 - 评审结果三方案:方案评审通过 / 方案局部修改无需再审 / 方案重大问题需二次评审 - 报告:网页版 + Word版 + PDF版(libreoffice 转换);首部公司信息"慧飞数字科技(秦皇岛)有限公司";要点表状态独立列;末尾签字确认区 - 数据:~/docs/review/review.db(meetings/points 两表)+ attachments/ - ⚠️ 服务运行在 ctwing-2 venv(非 huifei venv),python-docx 须装在该 venv ===== 8. 库房集成(2026-08-31 迁移) ===== - rd_store.py 改 Blueprint(bp)挂载到 rd_h5 统一进程(13544);rd-store.service 已停(13545 退役) - 库房独立运行兼容:`STORE_DB=/tmp/x.db STORE_PORT=13599 python rd_store.py`(测试隔离) - 底部导航(任务/库房/我的):任务=视图切换;库房=跳 /store/;我的=账户信息 - 库房页登录校验:同域读 localStorage rd_user,未登录显示橙色提示条(不覆盖页面) - 库房 tabbar 用 div(**不能用 nav**——库房 CSS 有 nav 标签选择器 sticky 会覆盖 fixed) ===== 5. 底部导航(3 tab) ===== [📋 任务] [📦 库房] [👤 我的] - 我的:账户信息(姓名/身份/钉钉ID/登录方式/我的任务数/权限)+ 账号管理(管理员)+ 返回首页 + 退出登录 - 普通用户无「切换身份」「账号管理」 ====== 五、API 列表 ====== | API | 方法 | 说明 | |-----|------|------| | /api/tasks | GET | 任务列表(?user= 过滤:普通用户只看自己;admin 全量)| | /api/tasks | POST | 新增任务(记录 creator)| | /api/tasks/ | POST | 更新任务(can_edit 校验)| | /api/tasks/ | DELETE | 删除任务(can_delete 校验)| | /api/tasks//note | POST | 添加更新记录(can_edit)| | /api/tasks//note/ | DELETE | 删除记录(can_edit)| | /api/tasks//subtask//note | POST | 子任务更新(can_edit)| | /api/me | GET | 当前用户信息(role/perms)| | /api/accounts | GET/POST | 账户列表(管理员)/配置权限(管理员)| | /api/login_pwd | POST | 口令登录(备用)| | /api/set_pwd | POST | 设置口令(管理员)| | /api/people | GET/POST | 人员库 | | /api/ding/login | POST | 免登 authCode→身份 | | /api/ding/org | GET | 钉钉通讯录(读 ding_org.json)| | /api/ding/oauth/url | GET | OAuth 授权 URL | | /api/ding/oauth/cb | POST | OAuth 授权码→身份 | | /api/upload | POST | 附件上传 | | /api/report/today | GET | 今日日报(含当日请假区)| | /api/report/week | GET | 上周周报(项目推进/管理推进/日报)| | /api/tags | GET/POST | 标签库(列表/自定义新增)| | /api/leave | GET | 请假记录(?date= 按天过滤)| | /api/store/* | * | 库房 API(samples/requests/approve/destroy/back/back_apply/ledger/history/...)| ====== 六、关键规则(用户拍板) ====== 1. **管理员**:武燕青——查看/编辑所有任务 + 账号管理;普通用户只见/只编辑自己任务 2. **只读模式**:?ro=1(共享链接);**登录用户自动退出只读**(exitReadonly 恢复标题/按钮) 3. **任务负责人**:存简称(张/席),登录用全名——匹配用 name_variants(简称↔全名变体) 4. **普通用户不显示**:切换身份、账号管理、口令登录入口(唯一登录=钉钉认证) 5. **日报**:生成今日日报(按任务/工时);员工日报汇总 21:30 推送(cron 52967bf00170) 6. **钉钉通讯录**:MCP 同步到 ding_org.json(cron 8f32fd550f80 每周一 06:00) 7. **owner 一律全名**(2026-09-02 定稿,不用简称);前端 shortOf 全名→简称映射兼容分组 8. **列表排序**:同优先级内新任务在前(id 降序) 9. **删除/编辑 API 必须传 user**(delTask/delNote 带 {user},否则 403) 10. **附件查看**:链接加 download 属性(内置浏览器无法预览 docx,直接下载) 11. **子任务新增时间(2026-09-03 用户定稿)**:= 子任务名首次出现在更新记录(notes)中的日期;无记录才回退 created 字段。⚠️ 曾 bug:保存任务时旧子任务 created 被重设为当天(后端已修:同名同负责人保留旧 created);历史污染数据已按 notes 首现批量修正 12. **项目列表**:PROJ_LIST 常量(拓远T1/普惠P1/金穗S1/LoRa网关/P3/P5/流量计/表压/软管泵/施肥阀/施肥机/其他) 13. **复盘 cron 已暂停**(b9dbd39c3f34,2026-09-03 用户要求不再定时推送,可手动 run) 14. **测试报告导出**(2026-09-06):/api/test/report/?mode=&fmt=html|docx|pdf(横向 Word 含照片)+ /api/test/report/full?fmt= 整机;前端按钮 📄网页/📝Word/📕PDF 15. **评审会导出**(2026-09-06):/api/review/report/?fmt=html|docx|pdf 16. 底部导航 5 tab 三处同步(rd_h5.html / rd_store.html / test.html / review.html 各自的 tabbar) ====== 七、部署运维 ====== sudo systemctl restart rd-h5.service # 重启 sudo journalctl -u rd-h5.service -n 50 # 日志 sudo systemctl reload caddy # Caddy 重载 sudo vi /etc/caddy/Caddyfile # 域名配置(rd.huiflytech.com → 13544) git -C /home/ubuntu add -A && git -C /home/ubuntu commit -m "..." # 版本管理 - 证书:Caddy 自动(Let's Encrypt);阿里云手动证书包可留档备用 - 域名 DNS:rd.huiflytech.com → 82.156.85.124(huiflytech.com 已备案,80/443 不被劫持) ====== 八、排障速查 ====== | 现象 | 原因 | 解决 | |------|------|------| | 钉钉打开不自动登录 | 应用类型/权限 | JSAPI 免登需钉钉 JSAPI 安全域名;应用需在钉钉后台配置 | | 免登报"不合法的appKey" | 凭证错 | .env CLIENT_ID(ding 开头)+ SECRET 匹配 | | 通讯录同步空 | 应用未开通通讯录权限 | 钉钉后台开通 qyapi_get_department_list/member;或 MCP 同步 ding_org.json | | 管理员变"只读" | applyPerm 用 people.json roles(空)| isAdmin 已改账户体系判定(名字=武燕青)| | 普通用户看到全部任务 | view_own_only 未生效 | accounts.json perms;后端 is_mine 过滤 | | 任务匹配不上 | owner 简称 vs 全名 | name_variants 变体匹配 | | 库房 tabbar 跑中间 | nav 标签选择器覆盖 | tabbar 用 div 类(不用 nav)| | 库房内容空白 | 登录校验覆盖 body | 已改提示条(不覆盖)| | 页面旧版 | 浏览器缓存 | 强刷 / 加 ?v= 参数;服务端 no-store | ====== 附三:新增任务权限调整 + /api/people 角色来源修复(2026-09-16) ====== **问题**:刘瑞红"新建了任务但没显示" → 排查发现**数据里根本没有**她建的任务。 **根因**: 1. `accounts.json` 的 `roles.admin` 只有「武燕青」→ 刘瑞红是普通用户 → 前端 `applyPerm()` 执行 `['btnAdd','btnPeople'] 隐藏` → **她看不到「+ 新增任务」按钮**,建不了任务 2. ★ 更深一层:`/api/people` 的 `roles` 读的是 **people.json**(其 `roles.admin` 为空),而后端权限判定用 **accounts.json** → **前端 ADMINS 永远为空** → 即便把某人加入 accounts.json 的 admin,前端仍按普通用户处理(双侧数据源不一致的隐蔽 bug) **修复**: - `accounts.json`:`roles.admin` 增加「刘瑞红」(从 user 列表移除) - 前端 `rd_h5.html`: - `applyPerm()` 不再隐藏 `btnAdd`(**新增任务对全员放开**);`btnPeople`(人员管理)仍限管理员 - `openAdd()` 负责人选择器:非管理员**默认预选自己**(`_defOwn=[myShort()]`),避免建完因筛选自己而看不到 - 后端 `/api/people`:`roles` 改为 `load_accounts()` 读取(与权限判定统一) **验证**:`/api/people` → admin=`['武燕青','刘瑞红']` ✅|`/api/me?user=刘瑞红` → role=admin + perms 全开 ✅|叶小军 → user(edit_own/view_own_only 保持)✅|页面 JS 语法 ✅ **⚠️ 教训**:**权限相关的数据源必须唯一**(accounts.json)。前端拿角色的接口(/api/people)与后端判权的函数(is_admin)读同一份文件,否则会出现"加了管理员但前端不认"的隐蔽问题。 ====== 附四:新建人显示 + 期望完成时间 + 普通用户硬隔离(2026-09-16) ====== **① 任务显示新建人**(`creator` 字段,存全名) - 后端 `add_task` 写入 `creator`(取 `creator`/`user` 参数);`update_task` 仅在库中为空时补写(不覆盖) - 前端:任务卡片(跟踪 + 临时)显示 `👤 新建:XXX`;详情页显示 `👤 新建:XXX(创建日期)`;只读详情同样显示 - 新增任务提交时带 `creator: USER_NAME` **② 期望完成时间**(`due_date`,YYYY-MM-DD) - 后端 add/update 支持该字段 - 新增任务表单:跟踪任务增加「期望完成」日期选择(可选) - 详情页:可编辑(`#mDue`),保存随 `saveTask` 提交 - 卡片显示:`🎯 期望完成:2026-09-20`;**已逾期(due_date < 今天且未完成)标红并附「(已逾期)」** **③ 普通用户可见性(2026-09-16 定稿)** > 规则演进:① 最初「自己负责 或 自己创建」→ ② 用户一度要求收紧为仅负责人 → ③ **最终定稿:创建人可见自己创建的所有任务,其他用户(既非负责人也非创建人)不可见** - 全局开关 `MY_ONLY`:非管理员登录时置 `true` - `baseFilter()` 首行强制过滤:`if(MY_ONLY && !(isMine(t) || isCreatorOf(t))) return false`(不受人员筛选/搜索影响) - `isCreatorOf(t)`:`t.creator` 与登录人全名/简称双向匹配 - 分组:`XXX 主负责` | **`XXX 我创建(他人负责)`**(绿色 #26a69a)| `XXX 负责子任务` - 权限:`canEditTask` = `isMine` **或** `isCreatorOf`;后端 `can_edit` 同步 = `is_mine or is_creator` - 后端 `is_creator(task,user)` 用 `name_variants` 做全名/简称匹配 - ★ **后端 `/api/tasks` 同步过滤(根本保证)**:`if user and not is_admin(user) and perm.view_own_only: items=[it for it in items if is_mine(it,user) or is_creator(it,user)]` - 前端 `MY_ONLY` 是展示层过滤,后端过滤才是根本保障(**改前端 JS 也无法拿到他人任务**) - 未登录 / 管理员 → 返回全量(只读看板 `?ro=1` 需要) **验证(#99 金穗S1单loraPCB板设计:owners=['张'] creator=武燕青)**——后端接口实测: | 身份 | 可见任务数 | #99 | |---|---|---| | 张景茹(负责人) | 35 | 可见 ✅ | | 武燕青(管理员/创建人) | 99 | 可见 ✅ | | 席江锋(其他普通用户) | 20 | **不可见** ✅ | | 叶小军(无任务) | 0 | 不可见(无任务)✅ | | 未登录(只读看板) | 99 | 可见 ✅ | > ⚠️ **测试提醒**:API 测试勿传空值字段(如 `name:''`)——update_task 会按字段更新导致清空;本次已发现并恢复 #99 名称。 ===== 附四补:分组判定 bug 修复(2026-09-16,用户截图报) ===== **现象**:管理员(武燕青)在页面上点选「潘保琰」筛选后,分组 **"潘保琰 我创建(他人负责)"** 里出现了 **#99 金穗S1单loraPCB板设计**(该任务实际 creator=武燕青、owners=['张'],与潘保琰无关)。 **根因**:`render()` 里 `made` 分组用的是 `isCreatorOf(t)` —— 它判定的是 **当前登录人**是否创建,而不是**被筛选人(SELP)**。 → 管理员筛他人时,会把**管理员自己创建的任务**错列到被筛人名下的"我创建(他人负责)"分组。 **修复**:新增 `isCreatorBy(t, who)`(按**指定人员**的简称/全名判定),`made` 分组改用 `isCreatorBy(t, SELP)`: function isCreatorBy(t,who){ var c=(t.creator||'').trim(); if(!c) return false; if(c===who) return true; // 简称相等 if(full(who)===c) return true; // 全名相等 return nameToShort(c)===who; // creator 全名 → 简称后比较 } var made=list.filter(function(t){return liu含SELP为假 && 子任务不含SELP && isCreatorBy(t,SELP)}); **验证(node 单测)**:筛选「潘保琰」→ #99 **不进入** made 分组 ✅|筛选「武燕青」→ 进入 ✅(正确) ====== 附五:任务主页分组折叠(2026-09-16) ====== **需求**:任务主页每个类别(分组)折叠显示。 **实现**(`rd_h5.html`): - 新增 `gkey/isCol/toggleGroup/gcolMap` 四个辅助函数 - `gkey(title)`:分组名转 key(非字母数字中文→`_`) - `isCol(k)`:**默认收起**(未记录视为 true=收起) - `toggleGroup(k)`:切换并写 `localStorage.rd_gcol`(`false`=展开,`true`/未记录=收起)后 `render()` - `render()` 中每个 `.group` 的 `.ghead`:加 ▸(收起)/ ▾(展开)指示 + 点击展开/收起;组内容仅在展开时渲染 - 覆盖:各负责人分组 + ✅ 已完成分组(key `__done__`) **验证(只读链接 ?ro=1 免登录实测)**: - 首次加载:7 个组全部 `▸` 收起(刘瑞红2|宋逊文2|席江锋13|张景茹9|武燕青19|潘保琰8|✅已完成45)✅ - 点击"刘瑞红":变 `▾` + 显示 2 项任务,其余组仍收起 ✅ - `localStorage.rd_gcol = {"刘瑞红":false}` 状态记忆正确 ✅ ====== 九、测试隔离 ====== - 任务:测试账号不写生产 rd_tasks.json(改前备份 git 可回滚) - 库房:`STORE_DB=/tmp/store_test.db STORE_PORT=13599 python rd_store.py` 独立实例,测完删库 - 严禁直接操作生产数据;测试数据即时清理 ====== 附:任务负责人被判只读修复(2026-09-15) ====== **现象**:张景茹(普通用户)打开自己负责的任务(如 #83),详情页显示"🔒 只读(仅管理员或任务负责人可编辑)",无法编辑(用户报)。 **根因**(前端 `rd_h5.html`): function canEditTask(t){ return (t.owner||'')===USER_NAME||(t.owners||[]).indexOf(USER_NAME)>=0; // ❌ } - `USER_NAME` 是**全名**("张景茹"),而任务 `owners` 存的是**简称**("张")→ 永不匹配 → 负责人被判只读 - ⚠️ 后端 `is_mine()` 用的是 `name_variants()`(全名+简称变体)✅ 本就正确 → **纯前端 bug** **修复**(新增两个函数 + 替换 3 处调用): function nameToShort(nm){ /* OMAP 反查全名→简称,不依赖 DING_USER */ } function isMine(t){ /* owners/owner/子任务负责 与 [全名, 简称] 双向匹配 */ } function canEditTask(t){ ... return isMine(t); } // 另 2 处"我的任务"统计:TASKS.filter(isMine).length **验证**: - node 单元测试 5/5:`owners:['张']` + 登录"张景茹" → true ✅|武燕青任务 → false ✅|子任务负责 → true ✅|无负责人 → false ✅|全名存储兼容 → true ✅ - 后端 `name_variants` 正确;`accounts.json` 中张景茹 `edit_own:true` ✅ - 三模块 200 ✅ **经验**:任务负责人存简称、登录用全名——**任何"是否是我的"判断都必须走 `isMine()/nameToShort()`**,禁止直接拿 `USER_NAME` 与 `owners` 比较。 ====== 附二:生成日报 500 修复(2026-09-15 晚) ====== **现象**:点「📋 生成日报」弹窗卡在"正在生成..."(用户报)。 **根因**: 1. **后端 500**:`rd_h5.py` 日报聚合里,子任务负责人"今日无更新但任务今日完成"时直接 `per[so]["done_today"] += 1`,而 `per` 中该键尚未初始化 → `KeyError: '刘'`(刘瑞红为子任务负责人) 2. **前端卡住**:`genReportDo()` 的 fetch 无 `.catch()` → 接口返回 HTML(500) 时 `r.json()` 抛错,弹窗永远停在"正在生成..." **修复**: - 后端:`if done_today:` 前补 `per_init(so)` - 前端:`genReportDo()` 增加 `.catch()` → 显示"⚠️ 生成失败:…"+关闭按钮 **验证**:`/api/report/today` 200(1096 字符,含负责人/工时/星级)|周报接口本就正常(已有 catch)|三模块 200 **经验**:① 聚合 per[键] 累加前一律先 `per_init` ② 所有 fetch 必须带 `.catch()`(否则失败时 UI 卡死无提示)