# microi-mobile-app-quality 详细参考 2

> 按需读取；本文件由 SKILL.md 的原章节无损拆分。

<!-- microi-progressive:chunk id=microi-mobile-app-quality-018 sha256=28b7c339cbf546a91f4f35fce2bc146f052283fad2b6c54d7a8971acde21740a -->
## 9. 主题切换必须真实且全局生效

当客户要求增加另一种视觉风格时，除非用户明确要求删除，否则要把当前已认可设计保留为一个命名主题，而不是直接覆盖。

要求：
- 主题命名要表达视觉意图，不要沿用临时客户措辞。例如：`清新绿红`、`品牌经典`、`专业深色`。
- 用 `uni.setStorageSync` 或项目主题运行时持久化主题选择。
- 如果产品有未登录也可打开的我的/个人中心/设置页，主题切换必须在登录前可用。
- 主题切换应是紧凑的“切换主题”操作，打开弹窗或底部面板；除非页面明确是完整设置页，否则不要把所有主题选项直接堆在我的/个人中心首页。
- 主题状态要作用到每个页面根节点、固定底部导航、空状态、骨架屏、加载过渡、按钮、卡片、报告详情和 H5 桌面手机壳。只改变当前页的主题是不完整的。
- 小程序构建不能只依赖 `document.documentElement`；应使用页面根类、CSS 变量或跨端主题服务。
- H5 uni-app 主题服务只能修改安全外壳：`html`、`body` 的 `data-*` 属性、主题 class 和 CSS 变量。不要通过 `querySelectorAll('.mci-page')`、`MutationObserver` 或定时扫描去改 `.mci-page`、`uni-page-body`、`uni-page`、`RouterView` 下的 Vue/uni 托管节点 class，否则容易触发 Vue 内部只读字段和空 vnode 错误。
- 固定底部导航、固定提交栏、悬浮操作条等 fixed 组件在 H5 中优先继承 `html/body` 上的 `--mci-*` 主题变量，避免因为组件自己订阅 theme store 或动态绑定 `bottom-nav--theme` class 导致导航时重渲染。小程序端可在组件根节点绑定稳定主题 class，但不要在 H5 路由切换过程中改变 Vue 托管根节点结构。
- 如果页面局部 scoped CSS 写死颜色，要增加主题覆盖或重构为 `--mci-*` 变量；骨架屏、加载流光、页面过渡遮罩、报告封面、英文小标题、印章/水印、摘要卡和富文本容器也必须使用主题变量。
- H5 主题变化不能破坏 uni-app 路由补丁。如果切主题后点击导航出现 `Cannot assign to read only property '_'`、`Cannot read properties of null (reading 'type')`、`parentNode`、`scheduler flush`、`updateSlots` 等错误，必须先移除对 Vue/uni 托管节点的 DOM 改写，保持页面 class 稳定，并改用 `html/body` 变量驱动主题。
- 底部导航必须防止重复点击当前路由、对连续点击做防抖，并在主题切换后略微延迟路由跳转，确保 DOM/主题更新先于 `uni.reLaunch` 完成。

禁止：
- 新增客户偏好主题时删除此前已认可设计。
- 主题切换效果在导航或重启后消失。
- 主题卡片/选项使用纯文字假图标。
- 主题切换破坏底部导航或产生 Vue scheduler 错误。
- 为了补主题而直接改 `.mci-page`、`uni-page-body`、`uni-page` 等运行时节点 class。
- 只让首页和列表页换主题，详情页、骨架屏、加载过渡或报告页仍残留旧主题。

验收：
- 在未登录的我的/个人中心页切换主题，并导航到首页、登录页、列表页、详情页和表单页。
- 确认底部导航、主按钮、卡片、空状态和页面背景都一致变化。
- 刷新 H5 页面或重启小程序后确认已选主题恢复。
- 对 `pages.json` 中每个路由、每个命名主题做截图验证。重点检查文字对比度，尤其是快捷卡片、报告卡片、空状态、底部导航、首屏文字和弹窗/底部面板内容。
- 主题切换后必须额外截图或断言：底部导航容器背景、未选中项文字、选中项文字、选中图标圆底、首页快捷入口图标颜色都已经切到当前主题，而不是残留旧主题。
- 主题切换后必须继续点击每个底部导航项，并打开至少一个详情路由（例如报告详情），断言控制台没有 `read only property '_'`、`null (reading 'type')`、`parentNode`、`scheduler flush` 等错误。
- 骨架屏和加载过渡要在切换主题后重新触发一次；颜色、流光、遮罩和空态不能残留上一个主题。
- 报告详情页必须逐主题截图，检查 `INSPECTION REPORT`、状态胶囊、封面标题、摘要卡、报告正文和富文本在当前主题下都有足够对比度。

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=microi-mobile-app-quality-019 sha256=406cf4edff553e80567a6462a68b872d98275358b89ff196df0873f78a2c30a9 -->
## 10. 报告/列表详情必须保留用户身份

从列表进入详情时必须保留调用者身份模型。即使打开的是同一个视觉报告详情页，员工、客户和公开/分享路线也可能需要不同接口。

要求：
- 如果员工通过已鉴权 FormEngine 或后台账号接口能看到报告列表，报告详情必须使用同样的员工鉴权路径或传有效员工 token。
- 客户打开报告时，使用客户 token 或感知绑定关系的接口引擎。
- 外部用户打开分享报告时，使用分享 token 路由，不要求员工/客户登录。
- 不要给员工用户发送空 `CustomerToken`，然后把后端响应解释为“未登录”。
- 打开详情页前保留当前会话；除非员工鉴权端点真实返回登录过期码，否则不要清除员工 token。

禁止：
- 员工列表点击时，直接复用客户匿名报告详情引擎且没有员工凭证路径。
- 登录用户点击已经可见的报告/列表项后被重定向到登录页。

验收：
- 分别测试员工列表 -> 报告详情、客户列表 -> 报告详情、分享 token 详情。
- 确认点击可见卡片后不会发生意外登录跳转。

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=microi-mobile-app-quality-020 sha256=c11ddf69390b21893b0bbb7de62592b8af6e9ecee5eb046bbeff99f3782e7493 -->
## 11. 角色与权限必须基于 sys_user.RoleIds 建模

移动端和后台不能只区分“已登录/未登录”。企业应用通常至少有内部员工、售后师傅、客服、客户账号等角色，必须在建模阶段明确角色、菜单权限和数据权限。

要求：
- 内部账号统一使用 `sys_user` 登录，身份可从 `RoleIds`、`_Roles`、`Roles`、`RoleName` 和 `Level` 推导展示能力；只有服务端已确认的 `Level>=9999` 才视为平台超级管理员。角色名和前端 `_IsAdmin` 不能代替接口授权。
- 角色能力要在统一 session/capability store 中计算，例如 `isTechnician`、`isServiceAgent`、`isCustomerAccount`、`canAcceptOrders`、`canManageCustomers`、`canViewReports`。一个人有多个角色时取能力并集。
- 客户账号角色要精确判断，不能把“客户管理”这类内部后台角色误判为客户账号。建议只把“客户”“客户账号”“客户用户”或明确包含“客户账号”的角色当作客户侧账号。
- 后台通过 `sys_role` + `sys_rolelimit` 建角色和菜单权限。售后师傅通常能看维保计划、工单、维保记录、报修和检测报告；客服通常能看客户中心、工单、报修、报告和资讯；客户账号默认不直接授予后台菜单，客户侧数据通过绑定关系和接口数据过滤提供。
- 移动端按钮和数据必须按 capability 控制：接单按钮只给售后师傅/超级管理员，客户资料管理只给客服/超级管理员，客户账号只能看自己绑定客户的数据。
- 菜单权限只是粗粒度入口控制，接口引擎和 FormEngine 查询仍必须按角色、`V8.CurrentUser`、客户绑定关系做行级过滤。
- 微信小程序手机号授权登录后，如果当前 OpenId/小程序用户还没有绑定客户或内部 `sys_user` 身份，移动端不能只显示“联系管理员”。必须提供“申请绑定身份”入口，允许用户申请绑定客户、售后师傅或客服等角色，并把申请写入独立审核表；后台审核通过后才写客户绑定表或 `Sys_User.MiniProgramOpenId` 等正式身份字段。
- 身份绑定申请必须显示审核状态：待审核、已通过、已驳回。待审核期间不要提前开放客户数据、接单、工单处理等能力；已驳回要展示原因或允许重新提交。
- 后台审核必须由有权限的内部账号执行。审核客户申请时应由后台选择真实客户 Id 后再写绑定关系，不能只相信用户填写的客户名称；审核售后师傅/客服申请时应由后台选择真实 `sys_user.Id` 后再绑定 OpenId。
- 使用 MCP 建角色前先回读 `sys_role` 和 `sys_menu`。如果 `microi_save_role` 或通用 `add_form_data('sys_role')` 因 `UpdateTime cannot be null` 等系统字段问题失败，要使用平台修复后的专用角色工具；临时迁移可用一次性接口引擎和参数化 `V8.Db` 补建角色，但必须回读验证并删除或禁用临时入口。

禁止：
- 只做前端显示区分，后台角色和菜单权限不落库。
- 把客户账号当作普通后台用户直接开放客户、报告、工单全量菜单。
- 客户授权手机号登录后没有绑定关系时，只提示“联系管理员”，却没有申请、审核、状态回显闭环。
- 审核绑定时只按用户填写的客户名或姓名自动匹配，未由后台确认真实 `CustomerId` / `Sys_User.Id`。
- 多角色用户只按第一个角色判断，导致能力丢失。
- 登录状态、角色文本和页面权限由多个页面各自读取缓存，造成互相矛盾。

验收：
- 用超级管理员、售后师傅、客服、客户账号分别登录截图，确认姓名、角色、按钮和数据范围一致。
- 用多角色账号登录，确认能力取并集。
- 回读 `sys_role`、`sys_rolelimit`、关键接口返回数据，确认角色存在、菜单授权正确、客户数据没有越权。
- 使用真实账号登录后刷新 H5 或重启小程序，不能出现缓存用户名但状态未登录的半登录状态。
- 用未绑定的小程序手机号账号登录，截图确认“我的”页出现申请绑定身份入口；提交申请后后台能看到待审核记录；通过审核前数据权限不提前开放，通过审核后对应客户数据或内部工作台能力才出现。

<!-- /microi-progressive:chunk -->
<!-- microi-progressive:chunk id=microi-mobile-app-quality-021 sha256=e177d9cc3a9c28603129f42b598d2af72ecf305e934a982ed9801ca0d5a9e73c -->
## 审核后的会话与角色同步（强制）

- 审核通过后的数据库角色、服务端 DiyToken 身份投影、SDK 用户缓存、页面 capability 和申请状态必须一致，禁止以“退出重登后正常”作为验收成功。
- `RefreshLoginUser` 必须检查返回值和真实授权边界。普通审核员能审核业务申请，不代表其能刷新任意用户 Token；不得伪造管理员身份、手写登录 Redis 或扩大跨用户会话权限。
- 已授权的刷新应在角色事务提交后执行。平台仅允许普通用户刷新本人会话时，审核接口提交后发送定向身份变更通知，接收方调用固定的本人上下文接口，从主库重算角色并刷新本人全部有效终端会话；通知不能携带可直接采信的角色、Token 或权限。
- SignalR 推送只作加速。应用前台恢复、实时重连及有界前台校验必须能从权威数据恢复离线期间的审批；后台停止轮询。不得要求审核者与申请者位于同一 API 节点。
- 应用级会话同步实行 single-flight，刷新结果同时替换 SDK 和 store 用户缓存。退出或切换账号递增 generation，拒绝前一身份的迟到响应，不能让旧请求把旧角色写回来。
- 比较身份投影时排除 `$authenticated` 等调用级虚拟角色；它们不能保存为业务角色，也不能导致每次查询重复刷新会话。
- 带 `OnlyGet` 的客户角色需要调用身份申请等窄业务入口时，只为已经做服务端用户隔离的接口配置明确 `ApiRole`，不得移除客户角色的全局只读保护。
- 必测：两个独立登录上下文，一方保持原登录状态，另一方以真实审核员权限通过申请；前者不重登、不刷新页面获得新角色。继续验证原 Token、另一终端有效 Token、SDK 缓存、断网恢复、降权、退出时在途请求及未经授权的审核拒绝。

## 最终交付清单

移动端项目标记完成前必须确认：
- 图标：底部导航、首页快捷入口、个人中心快捷项、主按钮。
- 登录：H5/App 只有一条账号/手机号 + 密码路径；微信手机号授权是小程序默认路径；没有并列重复登录系统，没有租户/版本/调试块；SDK 接口已核验。
- 登录状态：成功登录必须同时有 token 和用户 `Id`；刷新、切换导航和进入“我的”页后，不得出现缓存用户名但仍提示未登录。
- 按钮：图标 + 文字、按下态、加载态。
- 菜单：后台规划为至少两级树。
- 请求头：`osclient` 是唯一标准请求头值，没有因大小写重复。
- 未登录态：授权/登录提示卡片和主按钮在可用内容区上下左右居中，不贴顶部。
- 首屏：首屏文字和悬浮快捷面板已截图检查，无裁切或重叠。
- 动效：入场、点击、骨架屏动画存在且克制。
- 主题：命名主题可持久化，未登录可切换，并影响所有页面和底部导航。
- 主题质检：每个 `pages.json` 路由在每个命名主题下都截图检查；切换主题再导航后，没有低对比文字，也没有 Vue scheduler/router 错误。
- 图标对比：首页快捷入口、底部导航、我的快捷入口在每个主题下都清晰可见；不得出现绿色圆底配绿色图标、灰色圆底配低对比图标等情况。
- 鉴权质检：列表到详情路由保留员工/客户/分享身份，不会把已可见项目重定向回登录页。
- 分享：`pages.json` 中每个页面都已注册好友转发和朋友圈生命周期；登录/受保护页面也可分享，接收者进入后再鉴权。
- 登录诊断：体验版手机号登录失败时显示阶段、原因和追踪号；后台系统日志可按追踪号查到脱敏错误上下文。
- 角色权限：`sys_user.RoleIds`、`sys_role`、`sys_rolelimit`、前端 capability 和接口行级过滤已联动验证。
- 验证：脚本存在时运行 `build:h5`、`build:mp-weixin` 和 `build:app`；执行 H5 路由冒烟测试和控制台检查。
<!-- /microi-progressive:chunk -->
