Site Tools


Hotfix release available: 2026-07-14c "Mort". upgrade now! [57.3] (what's this?)
研发任务管理系统h5:冷启动文件

Table of Contents

研发任务管理系统 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:`<select id=“fProj”>` → `<input id=“fProj” list=“projDL”>`(可下拉选、可直接打字);右侧出现「+ 新建项目」链接 - `renderProj()` 改为填充 `<datalist id=“projDL”>`;`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/<id>/note` 200、`POST /api/tasks/<id>` 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/<id>` 的 UPDATE 增加 `photos=?`(沿用旧值兜底);GET 用 `SELECT *` 自动带出。 上传复用已有 `POST /api/review/upload` → 返回 `/review/attachments/<file>`。

前端(`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(保存) → .then1)`。

 `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/<id>`、`?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:任务/库房/测试/评审会/我的) - 会议:新建/继承(复制全部内容+议题自动续序号)/项目/开始时间/预计时长/结束时间(自动算实际)/会议人员(钉钉多选)/负责人/评审阶段/标签

  1. ★ 评审阶段(2026-09-16 更新,共 8 个):需求分析/可行性评估 → 立项评审 → 方案设计 → 详细设计 → 样机试制 → 测试验证 → 量产导入 → 结项评审
  2. 定义位置:`review.html` 的 `stageSel`(唯一来源,改这里即可)

- ★ 与会人员手写签名(2026-09-16 新增):

  1. 前端:编辑页「与会人员签名」区 → 与会人员一人一张卡片(未签名/已签缩略图)→ 点「✍️ 签名」弹出手写板(canvas 900×360,支持手指触摸与鼠标,可清除重写)
  2. 存储:`meetings.signatures`(TEXT,JSON `{人名: “data:image/png;base64,…”}`);保存接口仅当传入非空才更新(防前端未带字段时清空)
  3. 人员来源:`meetings.members`(分号/逗号分隔)解析
  4. 报告:Web 内嵌 `<img>`;Word/PDF 用 `add_picture` 插入真实签名图(原为“打印后手写签字”的空白表)
  5. 相关函数:`sigLoad/renderSigns/openSign/initPad/clearPad/savePad/clearSign/closeSign`;后端 `_parse_sig/_sig_one/_sig_block/_sig_docx`
  6. ★ 自动记录签名时间(2026-09-16):签名为 `{人名: {“img”: “data:image/png;base64,…”, “time”: “YYYY-MM-DD HH:MM”}}`(兼容旧格式纯 base64 字符串,无时间不报错);前端签名卡片显示 `🕐 时间`;报告 Web/Word/PDF 均在签名下方显示时间
  7. ★ 签名后自动提交:`savePad` 完成后自动调用 `saveMeeting()` 落库(避免忘记点保存导致报告无签名)
  8. 实测:3 人签名(2 新格式含时间 + 1 旧格式)→ Web 报告 3 张签名图 + 时间正确显示 ✅|Word 内嵌 3 张图 + 含时间 ✅|PDF 69KB ✅

- ★ 报告要点表按评审阶段动态列(2026-09-16):

  1. 判定:`_pt_need(s)` = 阶段含「需求分析」;`_pt_cols(s)` 返回列定义
  2. 需求分析/可行性评估阶段 → 精简为 4 列:序号 | 记录内容 | 日期 | 负责人(去掉 状态/优化方案/记录图片/附件)
  3. 其他阶段 → 保持 8 列(序号/记录内容/状态/优化方案/记录图片/附件/日期/负责人)
  4. 生效范围:Web 报告 + `_export_docx` + `_export_pdf`(共 3 处)
  5. 实测:需求分析阶段 Web/Word 表头均为 4 列 ✅;立项评审阶段对照仍为 8 列 ✅

- ★ 签名合并机制(2026-09-17 修复覆盖丢失 bug):

  1. 现象:#9 会议中超哥(席江锋)的手写签名消失(DB 仅剩武燕青/刘瑞红)
  2. 根因:前端 `SIG` 是打开页面时的快照 → 保存时整体覆盖 `signatures` 字段;多人用各自设备签名时,后保存者会把先前他人的签名冲掉
  3. 修复:后端改为按人名合并(`_merged = dict(old); _merged.update(new)`);传入 `{“人名”: null}` 表示删除该人签名
  4. 前端:新增 `SIG_DEL` 删除标记数组;`saveMeeting` 提交 `{本机已有签名…, 已清除人名: null}`
  5. 实测(模拟三设备并发):甲签 → {甲};乙用旧快照签 → {甲,乙} 未丢失 ✅;丙签 → {甲,乙,丙};清除乙 → {甲,丙} ✅
  6. ⚠️ 已丢失的签名无法恢复(无备份)→ 需当事人重新签名
  7. 实测:签名 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/<tid> POST 更新任务(can_edit 校验)
/api/tasks/<tid> DELETE 删除任务(can_delete 校验)
/api/tasks/<tid>/note POST 添加更新记录(can_edit)
/api/tasks/<tid>/note/<idx> DELETE 删除记录(can_edit)
/api/tasks/<tid>/subtask/<idx>/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/<sid>?mode=&fmt=html|docx|pdf(横向 Word 含照片)+ /api/test/report/full?fmt= 整机;前端按钮 📄网页/📝Word/📕PDF 15. 评审会导出(2026-09-06):/api/review/report/<mid>?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`:

  1. `applyPerm()` 不再隐藏 `btnAdd`(新增任务对全员放开);`btnPeople`(人员管理)仍限管理员
  2. `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)]`

  1. 前端 `MY_ONLY` 是展示层过滤,后端过滤才是根本保障(改前端 JS 也无法拿到他人任务)
  2. 未登录 / 管理员 → 返回全量(只读看板 `?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` 四个辅助函数

  1. `gkey(title)`:分组名转 key(非字母数字中文→`_`)
  2. `isCol(k)`:默认收起(未记录视为 true=收起)
  3. `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 卡死无提示)

1)
) ⇒ window.open(url
研发任务管理系统h5/冷启动文件.txt · Last modified: by 172.17.0.1