chore(工程组): 初始化根仓库并排除独立工程

This commit is contained in:
leon_mac
2026-10-18 15:11:44 +08:00
commit 8ae2838f78
32 changed files with 714 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# 小程序分包与交互整理 · 2026-10-14
目标:交互选项采用「标签 / 当前值 / 展开入口」的分组设置行,点击后通过编辑对话或选择弹层修改。不是 HTML/uni form 表单提交。沿用米白与森林绿,减少首页操作密度,主包只保留四个 tab 入口和浏览所需内容。
## 路由
主包 pages/gallery/index、pages/make/index、pages/store/index、pages/mine/index。
- packages/creation/config:mode=image|text|blank,设置生成参数;packages/creation/editor:调整画布,恢复与保存草稿。
- packages/patterns/detail:id,跟做图纸;packages/patterns/materials:id,用料清单;packages/patterns/print:id,打印参数与申请。
- packages/store/visit:桌位确认;packages/store/request:kind?/patternId?,服务申请;packages/store/records:记录与取消。
- packages/account/library:type=works|favorites,个人图纸。
每个分包页显示返回上页与返回所属主 tab 的入口。主 tab 用 switchTab,普通分包页用 navigateTo;无历史页栈时返回所属主 tab。不使用独立分包;共享基础 UI、Pinia 与 API 放主包可引用目录。旧 /pages/detail/index 不再注册,所有调用更新。
## 共用接口
- components/ui/SettingRow.vue:props label,value?,description?,disabled?,danger?;事件 activate。
- components/ui/EditDialog.vue:props open,title,value?,placeholder?,type?(text/number/digit/textarea),maxlength?;事件 close、confirm(value:string)。临时修改在确认后才提交。
- components/ui/ChoiceSheet.vue:props open,title,options:Array<{value:string,label:string,description?:string,color?:string}>,selected?:string[],multiple?:boolean;事件 close、confirm(values:string[])。
- components/ui/PageReturn.vue:props tab(gallery/make/store/mine);上页/主 tab 两个入口。
- core/navigation.ts:route(path,query?) 生成经过编码的分包 URL;goPage(path,query?);goMain(tab);backOrMain(tab)。主包不得静态导入分包代码。
- 通用样式 .page .page-heading .eyebrow .title .muted .settings-group .section-title .panel .row .spread .mt .primary .empty .error .hint .action-footer。
## 实施与验证
- [x] 共享设置行、对话框、选择面板与导航辅助;路由编码/回退测试。
- [x] 制作入口 + 生成参数页 + 编辑页,保留图片/文字/手绘、草稿与保存。
- [x] 门店入口 + 到店/申请/记录,保留幂等请求与状态反馈。
- [x] 图纸/我的主包页,以及详情/用料/打印/个人列表分包。
- [x] 核心逻辑测试、类型检查、H5 与 mp-weixin 构建;浏览器验证入口→分包→对话→返回主 tab,检查分包产物。
不改变后端接口与数据库。微信真机分包加载、原生对话与设备字体仍需最终真机验收。没有 Git 仓库,不创建虚构提交或分支。
## 验收结果
2026-10-14:`npm run typecheck`、`npm test`(23 项)、`npm run build:h5`、`npm run build:mp-weixin` 通过。微信构建产物 `app.json` 保留 4 个主包页面,并注册 4 个分包、9 个分包页面;每个注册页均生成 WXML。源代码没有 `<form>` 或旧 `/pages/detail/index` 路由。
浏览器检查覆盖 390px 手机与 320px 窄屏:
- 制作入口 → 参数页 → 编辑画布;取消修改保留 29 格,确认后变为 32 格;空白画布显示完整网格。
- 设置行支持 Enter,编辑对话支持 Escape 关闭并恢复焦点;320px 对话无横向溢出。
- 直接刷新编辑分包后,返回按钮在无历史栈时切回制作主 tab。
- 图纸 → 用料 → 返回详情;打印方式选择后只显示相关参数;到店打印将 `kind=print` 和图纸 ID 带入服务申请。
- 服务备注确认后,到店页往返保留服务类型、图纸与备注;分包的门店首页入口回到主 tab。
- 我的收藏迁入独立列表页,未登录状态和返回我的入口正常。
状态回归修复:离店/换桌位清除旧申请成功结果;换账号清除旧草稿、记录及幂等上下文,旧账号在途读写响应不再回写。首次未登录填写的申请在确认到店流程中保留。
本轮浏览器没有提交员工呼叫或创建服务器图纸;申请及创作工作流使用隔离测试验证。真实微信小程序的软键盘、相机、读屏与分包下载仍需开发者工具/真机验收。本轮没有完整 Lighthouse 或 Core Web Vitals 测量,不宣称达到评分或 WCAG 认证。
截图:[门店首页](screenshots/ui-store-2026-10-14.jpg)、[320px 编辑对话](screenshots/ui-dialog-320-2026-10-14.jpg)。
+39
View File
@@ -0,0 +1,39 @@
# 小程序依赖审计与修复
2026-10-03:仅修改 `perler-beads-applet/package.json` / `package-lock.json`,没有修改业务源码或配置。
## 官方版本核查
npm 官方注册表的 `@dcloudio/vite-plugin-uni` 最新稳定匹配套件为 `3.0.0-5020620260917001`(版本日期 2026-09-17),已统一更新 uni-app、uni-components、uni-h5、uni-mp-weixin、vite-plugin-uni、uni-cli-shared 六个直接依赖。npm `vue3` 标签实际是 `3.0.0-alpha-5030120260930001`,未采用 alpha;`latest` 标签也是旧 alpha,不能直接作为稳定选型依据。
当前稳定 uni 插件仍声明精确 peer Vite 5.2.8。通过 package overrides 显式采用同主版本 Vite 5.4.21,未使用忽略 peer 的强制安装,也未跨主版本替换 uni/Vite。保持 Vue 3.4.38 与现有工具链一致。
## 已修复的依赖链
覆盖依赖固定版本:Vite 5.4.21、ws 8.22.0、jpeg-js 0.4.4、PostCSS 8.5.28、postcss-selector-parser 6.1.4、Express 4.22.3、adm-zip 0.6.1、phin 3.7.1、Babel core 7.29.7,以及 intlify 9.14.5 的 core-base/shared/message-compiler/message-resolver/vue-devtools。同主版本升级;adm-zip/jpeg-js 属 0.x 补丁套件,其使用由生产构建验证。没有接受 `npm audit fix --force` 建议的 DCloud 0.x 降级。
清除了旧 i18n 原型污染/XSS、ws 内存泄漏/DoS、JPEG 解码无限循环、PostCSS 源映射读文件/输出 XSS、ZIP 解压相关问题以及 Express 路由链问题;Vite 5 的历史漏洞大部分修复。
最后一次 `npm audit --json`:24 项(9 high、15 moderate、0 critical、0 low)。审计统计包含传播至 DCloud 上层包的重复节点,不等于 24 个独立攻击入口。安装时统计曾为 22 项,最终以上述显式重新审计结果为准。原始报告约 45 项 / 20 high,明显降低但没有达到零风险。
## 残留与实际可达性
- **braces 3.0.3(high)**:npm 官方已发布最新仍为 3.0.3,审计范围 `<=3.0.3`,没有可选修复版本。传播经过 micromatch→fast-glob、chokidar、unimport→DCloud。实际使用用于源码/目录 glob 和本地文件监听;项目没有把顾客输入交给这些 Node glob API。构建开发工具在不可信源码或极深 glob 输入下仍可能被耗尽栈。不能把它简单报告成“已修复”;待上游发布补丁。
- **Vite 5.4.21(high)**:残留优化依赖 `.map` 路径穿越,以及 Windows UNC/替代路径相关开发服务器漏洞。DCloud CLI `dist/cli/server.js` 明确调用 Vite createServer,因此 dev:h5 使用时实际可达。监听 127.0.0.1 限制远程直接访问,但不是对恶意页面/开发请求的完整免疫。生产交付是静态 H5 文件/微信构建,不运行 Vite 开发服务;不应将 dev:h5 暴露公网或用作生产服务。真正彻底修复需要上游支持新版 Vite,未经确认不强制跨主版本覆盖。
- **esbuild 0.20.2(moderate)**:残留为 esbuild 内置开发服务器跨来源请求问题。DCloud `uni-cli-shared/dist/esbuild.js` 使用 esbuild.build;检查未发现项目调用 esbuild.serve。Vite 工具链也持有 esbuild,但本次生产构建不运行其内置服务。保留上游版本,避免未经兼容验证的 0.x 次版本强制升级。
这些依赖仍由 uni 包传递,`npm audit --omit=dev` 不能简单证明产物安全;应按具体 Node API 与生产打包内容判断。未声称实际扫描微信运行时或完整源到汇的数据流。
## 验证
依赖安装成功。实际执行通过:
```sh
npm test # 当前 12 项测试
npm run typecheck
npm run build:h5 # Compiler 5.26
npm run build:mp-weixin # Compiler 5.26
npm audit --json --cache /tmp/perler-npm-cache
```
审计仍返回非零结果为上述残留;测试与构建通过不等价于清除这些风险。业务源代码当时由根任务继续修改,最终交付前根任务应在最终源码快照重跑。
+76
View File
@@ -0,0 +1,76 @@
# 拼豆私域小程序首期实施计划
**目标:** 建立 uni-app 顾客端、Gin 服务端和 Vue 3 店员后台,完成图纸制作/收藏/保存、店内服务及管理员处理流程。
**已确认:** 用户已授权创建三个工程并编码。首期单店,数据预留多店隔离。支付与储值仅保留禁用的能力接口。内外网独立 HTTPS 域名;非 Wi-Fi 不探测内网。Wi-Fi 下短超时验证内网门店服务,失败回退公网。不读取 SSID。
## 全局约束
- 三个工程:perler-beads-applet、perler-beads-server、perler-beads-webui。
- 小程序 uni-app + Vue 3 + TypeScript;后台 Vue 3 + Element Plus + Pinia + Vue Router;禁止引入 Tailwind。
- 服务端 Gin + PostgreSQL + Redis,业务持久化以 PostgreSQL 为准,不以 Redis 为持久化真源。
- 所有 JSON 接口采用 HTTP 200 与 `{code,msg,data}`,code=200 成功、403 未登录、401 未授权;平台登录密钥不交给前端。
- 商户配置、图纸数据和权限必须在服务端校验;网络连通性不授予员工权限。
- 无微信凭据时提供显式开发模式,生产模式禁止开发登录。
- 首期支付/储值不可操作,返回 enabled=false;不建立假成功支付或余额。
- 首期打印为可下载 SVG 实际尺寸文件 + 店员手动打印任务;不宣称已自动驱动硬件。
- 原始图片在小程序本地处理;保存到服务端的是色号网格和元数据。演示色板与图纸明确标记,正式色板由后台维护。
## 接口契约
基础路径 `/api`。所有 ID 均 string。JSON 字段 camelCase。数组接口直接返回数组,不包 items。
- GET /health:data={service:"perler-beads",storeId:"store-001",version:"0.1.0"},不含个人信息。
- POST /auth/login:{code:string} 微信登录;开发模式允许 {code:"dev-guest"},data={token,user:{id,name,role,storeId}}。
- POST /auth/admin:{username,password},返回同登录形状;管理员凭据环境配置。
- GET /me:当前用户;GET /capabilities:{payment:{enabled:false},storedValue:{enabled:false}}。
- GET /stores:Store[];Store={id,name,address,hours,notice,localBaseUrl,publicBaseUrl}。
- GET /palettes?storeId=:Color[];Color={code,name,hex,location,available}。
- GET /patterns?storeId=&q=:Pattern[];GET /patterns/:id:Pattern。
- Pattern={id,title,category,width,height,cells:(string|null)[],colors:Color[],beadCount,ownerId?,visibility:"public"|"private",createdAt}。颜色和 cells 使用同一 code。透明为 null。
- POST /patterns(登录):{title,category,width,height,cells,colors,visibility:"private",storeId};DELETE /patterns/:id(所有者);GET /my/patterns。
- GET /favorites:string[];PUT /favorites/:id:{active:boolean}。
- POST /visits:{storeId,tableCode},data=Visit;GET /visits/current:Visit|null;DELETE /visits/current 退出;Visit={id,storeId,tableCode,status:"active"|"closed"}。tableCode 来自后台已启用桌位。
- GET /tables?storeId=(员工):Table[];Table={code,label,enabled}。
- GET /service-requests:顾客自己的任务;员工当前门店任务。ServiceRequest={id,storeId,userId,tableCode,kind:"help"|"refill"|"ironing"|"print",note,status:"pending"|"accepted"|"completed"|"cancelled",patternId?,createdAt,updatedAt}。
- POST /service-requests:{kind,note,patternId?,requestId};要求有效到店 session,store/table 取 session。requestId UUID 持久幂等;重复呼叫返回原任务或业务冲突。重复键不同内容不能误判为同次请求。
- PATCH /service-requests/:id:{status};员工可 pending->accepted->completed;顾客仅取消自己的 pending。校验门店。
- GET /admin/patterns;POST /admin/patterns;PATCH /admin/patterns/:id(发布/下架、名称分类);仅管理员。
- PUT /admin/palette:{storeId,colors:Color[]};PUT /admin/store:Store;PUT /admin/tables:{storeId,tables:Table[]};仅管理员。
- GET /patterns/:id/print?pitchMm=5:鉴权访问对应图纸,返回实际尺寸 SVG 文件,含色号/坐标/色量与校准标尺,超大图说明分页;不接受未校验 ID 文件路径。
## Task 1:服务端与数据持久化
- [x] 为图纸校验、服务状态变更、权限、幂等建立失败测试,再实现。
- [x] 建立 Gin 模块、迁移、配置、鉴权、PostgreSQL 仓储、Redis 会话/限流适配及演示数据。
- [x] 实现以上契约,保证用户私有图纸隔离和门店任务隔离。
- [x] 提供 go test ./...、go vet ./...、构建验证;数据库集成测试使用独立测试库,缺环境明确报告。
## Task 2:小程序与图纸工具
- [x] uni-app Vue 3 工程,图纸/制作/门店/我的四个页面,详情/跟做页。
- [x] 先用 Node 测试网络路由:蜂窝零探测、Wi-Fi 成功、失败回退、none、unknown、网络切换取消过期结果、幂等重试。
- [x] 图片本地裁剪采样、色板量化、透明网格、文字生成、局部改色、计数、逐色高亮、草稿和进度保存。
- [x] 图纸列表搜索收藏、账号保存、桌码到店、呼叫/补豆/熨烫/打印、取消与状态反馈。
- [x] 本地/公网连接统一封装,配置合法 HTTPS 端点,开发 localhost 仅开发构建。
- [x] 执行核心逻辑测试、类型检查、H5 与微信小程序构建。
## Task 3:店员管理后台
- [x] 登录、路由权限、门店切换边界、待处理服务列表、接单完成操作。
- [x] 图纸管理与导入、色板与物料位置编辑、桌位、门店配置、禁用支付能力展示。
- [x] Element Plus 控件与表格、侧栏、抽屉编辑,响应式、空/错/载入状态、无障碍标签。
- [x] 类型检查、构建、桌面/移动浏览器交互核查。
## Task 4:交付与整体验证
- [x] Docker Compose(PostgreSQL、Redis、服务端),双 HTTPS 域名与 frp 示例、环境变量模板。
- [x] README:本地启动、微信配置、示例色板限制、打印方式、真实部署验证清单。
- [x] 复查 API 联通、鉴权、重复呼叫与网络回退;独立代码审查,修复重要问题。
- [x] 记录真实执行结果;微信真机、实际 DNS/TLS、打印硬件未验证则明确列出。
## 进度
- 2026-10-03:用户确认开始编码。当前仅有 example 截图,无 Git 仓库、无已有代码;直接在指定工程目录创建,不创建额外 worktree。
- 2026-10-03:三端首期实现完成。最终类型检查、构建与测试通过,真实 PostgreSQL/Redis 联调及独立审查通过。部署模板已交付但未在 Docker 或门店网络运行;真机与打印验收仍待实际环境,详见 verification.md。
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+48
View File
@@ -0,0 +1,48 @@
# 服务端交付报告
实现目录:`perler-beads-server`,Go 1.25 + Gin + pgx PostgreSQL + Redis。全部 JSON 接口为 HTTP 200 与 `{code,msg,data}`;code=403 未登录、401 未授权。所有计划接口已经注册。打印成功为 SVG 下载。
## 启动
先启动 PostgreSQL 和 Redis,使用 `.env.example` 配置 `DATABASE_URL`、`REDIS_URL`、`ADMIN_USERNAME`、至少 12 字符的 `ADMIN_PASSWORD`、`APP_ENV`,再执行 `go run ./cmd/server`。应用不会自动读取 `.env`,请通过容器环境或 shell export 注入。生产环境必须提供 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`;仅 `APP_ENV=development` 接受 `dev-guest`。开发登录是共享演示身份,不应用于真实顾客。
启动时执行内嵌幂等迁移 `internal/server/migrations/001_initial.sql`。`SEED_DEMO=true` 在没有门店时写入 store-001、两个启用桌位、四张原创 7×7 像素图纸和十种 DEMO 色号。演示地址、营业时间和色号必须在上线前替换。生产环境不提供内存数据库降级。
## 数据与权限
业务数据持久化 PostgreSQL;Redis 只保存随机令牌会话与登录限流。令牌 SHA-256 哈希作为 Redis 键,有效期七天。微信 code2Session 请求只在服务端执行,openid 派生稳定用户 ID,平台密钥不返回。管理员账号从环境获取。默认不信任 X-Forwarded-For,以免伪造 IP 绕过限流;部署在代理后登录限流按代理源 IP 聚合,请在真实网络压力验证中评估并配置可信代理支持。
图纸大小限制 200×200,色板最多 512 色;色号必须唯一、hex 必须六位十六进制,cells 数量必须匹配尺寸,所有非透明格必须引用已定义颜色。保存图纸必须匹配当前门店维护色板,物料位置由服务端权威数据回填。顾客保存只能 private,所有者才能删除;私有图纸访问验证身份,工作人员只能访问所属门店。后台账号当前属于 store-001,无跨店授权。
桌位必须存在且已启用。服务请求从有效到店会话读取门店与桌位,提交时再次校验桌位;打印请求必须引用有访问权限且属于当前门店的图纸。requestId UUID 由数据库主键持久保证幂等,同用户并发请求通过事务锁串行;相同键不同 kind/note/patternId 返回 code=409。重试返回原始创建结果。员工状态只能 pending→accepted→completed,顾客仅取消自己的 pending。状态更新使用行锁,避免并发覆盖。
SVG 标注毫米尺寸、网格色号、坐标、色量、50 mm 校准标尺,pitchMm 为 2–10。文字转义,图纸 ID 不拼接文件路径。临时文件下载后一分钟清理。超大图注明打印机海报/分页模式并保持 100% 缩放。服务请求是人工打印任务,不驱动硬件。支付与储值 capability 恒为 enabled=false。
## 验证
先写图纸校验、授权状态机、幂等内容散列测试,执行时出现 undefined Pattern/Transition/Fingerprint 的预期失败,再实施。随后增加匿名 API 权限、私有图纸门店隔离、SVG 尺寸与转义检查。
实际执行通过:
```sh
GOPATH=/tmp/perler-go GOCACHE=/tmp/perler-go-cache GOMODCACHE=/tmp/perler-go-mod go test ./...
GOPATH=/tmp/perler-go GOCACHE=/tmp/perler-go-cache GOMODCACHE=/tmp/perler-go-mod go vet ./...
GOPATH=/tmp/perler-go GOCACHE=/tmp/perler-go-cache GOMODCACHE=/tmp/perler-go-mod go build -o /tmp/perler-beads-server ./cmd/server
```
`TestPostgresPersistentIdempotency` 无 TEST_DATABASE_URL 时跳过。执行该测试必须使用数据库名包含 test 的独立数据库;它检查数据库唯一键且回滚数据。初期系统没有 Docker/PostgreSQL/Redis,随后主任务使用临时 PostgreSQL/Redis 启动真实联调环境;独立 perler_test 数据库的集成测试实际通过。微信真实凭据、生产 DNS/TLS 和打印设备仍未验证。
## 审查修复
- 服务请求事务内读取图纸与桌位改用同一 tx,避免单连接池等待自己占用的连接;收藏列表采用单次 JOIN 查询,避免 rows 未释放时嵌套借连接。
- 启用 Gin ContextWithFallback,统一给请求 context 设置 15 秒业务操作期限;数据库、Redis 与微信请求继承客户端取消。事务回滚使用独立 3 秒清理 context。
- SVG 页面宽度至少 100 mm,50 mm 标尺完全位于页内;长标题按字符宽度换行,打印提示分行。图纸网格仍精确保持 pitchMm。新增最小图纸在 2/5/10 mm 的尺寸与文本边界测试。
- 演示数据扩为十色及四个原创像素图案(心、菱形、小花、星),全部 DEMO 标识。已初始化的库不会被 seed 覆盖,需要独立新库查看新演示数据。
- 新增 TestSingleConnectionHandlers:独立 TEST_DATABASE_URL 下将 pool MaxConns=1,有限时地验证收藏、打印服务创建与幂等重试;无数据库时明确跳过。
- 修复后 go test ./...、go vet ./...、go build 全部再次通过;随后主任务已使用独立 perler_test 数据库实际执行并通过 PostgreSQL 集成测试。
## 最终补充
`WEBUI_DIR` 可配置为后台构建输出目录(需包含 index.html),后端直接提供静态文件及非 API GET/HEAD 深层路由 SPA 回退。未知 /api 与 /api/* 仍返回 HTTP200/code404 JSON。路径验证限制在输出目录内,拒绝目录穿越与指向目录外的符号链接;新增临时目录静态文件、SPA、API 和符号链接测试。
自定义异常恢复统一返回 HTTP200/code500 JSON。SVG 标题、尺寸说明与坐标分开预留行,色号字体按长度缩放以限制在格内。上述修改的 go test/vet/build 已通过;主任务随后已切换到更新后的二进制,并再次通过 23 项 API 联调断言及最终真实数据库回归。
+79
View File
@@ -0,0 +1,79 @@
# 首期验收记录
日期:2026-10-03。此记录区分本机已验证内容与真实部署待验内容,不代表已发布微信小程序。
## 最终源码验证
| 检查 | 结果 |
| --- | --- |
| applet `npm test` | 12/12,通过;网络选择、过期探测、量化/透明像素/裁剪、画布偏移、幂等回退、提交后刷新失败 |
| applet `npm run typecheck` | 通过 |
| applet `npm run build:h5` | 通过,uni Compiler 5.26 |
| applet `npm run build:mp-weixin` | 通过,输出 dist/build/mp-weixin;未真机运行 |
| webui `npm test` | 3/3,通过;业务错误、鉴权和任务状态机 |
| webui `npm run build` | 类型检查与 Vite 生产构建通过 |
| server `go test -count=1 ./...` | 通过,设置 TEST_DATABASE_URL,真实执行数据库回归,未跳过数据库项 |
| server `go vet ./...` / `go build ./cmd/server` | 通过 |
| `scripts/integration-smoke.mjs` | 23 项 API 断言通过,4 个并发同键呼叫只创建一个任务 |
| frp 示例 TOML | Python tomllib 语法解析通过;未建立实际公网隧道 |
| Docker Compose / Nginx | 已提供配置,当前环境没有 Docker,未做容器与 HTTPS 启动验证 |
| 依赖审计 | webui 0;applet 24(9 high / 15 moderate),详见 dependency-report.md |
最终检查包含画布颜色从八位 hex 改为 uni legacy canvas 可识别 rgba 后的构建。开发服务重启以消除旧模块缓存,浏览器确认完成标记不再显示黑色块。
## 真实联调环境
临时 PostgreSQL 18(127.0.0.1:55432 / perler_test)、Redis(127.0.0.1:56379)与 Gin(127.0.0.1:8080);业务经过真实 PostgreSQL/Redis,没有前端 mock 或内存仓储替代。联调资源在 `/tmp`,不作为持久部署方案。测试密码仅用于这些本机隔离服务。
数据库测试要求 TEST_DATABASE_URL 指向名称包含 test 的独立数据库。单连接池回归验证收藏查询、图纸打印呼叫和重复提交不会嵌套借连接死锁;幂等测试验证持久唯一键。API 脚本需要显式测试地址及管理员密码:
```sh
PERLER_TEST_API_URL=http://127.0.0.1:8080 \
PERLER_TEST_ADMIN_PASSWORD='<隔离测试环境密码>' \
node scripts/integration-smoke.mjs
```
脚本仅针对 development 环境;它创建私有临时图纸、收藏、到店记录和服务任务,完成后移除收藏并删除测试图纸,保留到店会话与已完成任务作为验证记录。不要对真实门店数据执行该脚本。
## 浏览器验收
实际完成 H5 演示登录 → 文字「豆」生成 29×29 图纸(143 颗)→ 保存 → 收藏 → 详情跟做标记。滚动/偏移画布点击正确定位行 6 列 7。
顾客输入 T01 到店 → 呼叫员工并附备注「联调验证:请协助检查图纸」→ 后台真实管理员登录 → 接单 → 完成 → 顾客记录显示服务已完成。后台状态筛选可显示完成任务。浏览器还检查了桌面页面与 390×844 iframe 中的顾客首页、后台任务布局;iframe 检查是窄屏布局验证,不冒充手机微信真机测试。
截图:
- [顾客首页](screenshots/applet-gallery.jpg)
- [图纸跟做](screenshots/applet-progress.jpg)
- [后台完成记录](screenshots/admin-completed.jpg)
- [后台窄屏](screenshots/admin-mobile.jpg)
- [顾客窄屏](screenshots/applet-mobile.jpg)
未执行完整 Lighthouse / Core Web Vitals 实测、读屏器验收或所有设备组合。无障碍已做表单标签、导航/错误/空状态与原生控件基础处理;不能视为 WCAG 完整认证。
## 独立代码审查
独立只读审查后修复:事务中嵌套借数据库连接、请求 context 取消传播、打印页边界和标尺裁切、画布点击坐标偏移、呼叫成功后的刷新失败误报。修复后复核通过,无剩余重大问题。新增静态托管路径与符号链接隔离、业务异常统一响应的检查;未做独立渗透测试。
## 门店部署前验收
尚缺真实微信 AppID/密钥、域名和证书、门店网络与打印设备,以下无法在本次本机环境代验:
- Android/iOS 微信体验版真实登录,合法域名校验开启;图片选取、文字渲染、跟做点击与扫码。
- 蜂窝直走公网且零内网探测;其他 Wi-Fi 内网失败回退;门店 Wi-Fi 内网 HTTPS 成功;禁用局域网权限、DNS 不可达、证书错误、网络切换等场景。
- 内网域名证书可信且匹配域名,路由器 DNS/隔离策略允许访问;公网隧道与门店服务的连通、断线恢复。
- frp/Nginx/Compose 模板实际部署,生产密钥与管理员密码替换,APP_ENV=production,演示数据替换、备份恢复演练。
- 5 mm 与实际珠板尺寸匹配、50 mm 标尺实测、大图分页与真实打印设备输出。
- 多桌并发与员工轮询容量;反向代理下登录限流按代理源地址聚合,需按实际规模评估可信代理与限流策略。
当前首期为单店运行;数据归属与 API 预留门店边界,未来多店账号、选店、总部权限与多节点部署需另行实现。
## 后续增量:WebSocket + TTS(2026-10-03)
后台原 10 秒页面轮询已升级为工作台级 WebSocket 实时通知,60 秒兜底补查;增加本机中文语音提醒。后台测试现为 14 项,构建通过;服务端真实 PostgreSQL/Redis 测试及 race/vet/build 通过。完成顾客呼叫、实时列表与跨页面 TTS 的浏览器联调。详细范围、协议、证据与限制见 [语音提醒说明](voice-notifications.md)。
## 当前检查环境(2026-10-03)
按用户要求复用 events-server 的 PostgreSQL 实例 192.168.31.98:5433 与 postgres 用户,创建独立 perler_beads 数据库。后端已连接新库,验证 current_database/current_user 正确,4 张业务表与1个演示门店已初始化。events 库未修改,旧临时库测试数据未迁移。凭据保存在 Git 忽略的 perler-beads-server/.env.local,权限600。Redis 仍使用本机56379。
三端地址5173、5174、8080均返回HTTP200,管理员登录业务码200,通过5174代理的WS鉴权收到ready。三端以后台进程运行,日志位于 /tmp/perler-preview/。
+40
View File
@@ -0,0 +1,40 @@
# 后台呼叫语音提醒
## 设计与实施计划(2026-10-03)
目标:已登录的后台在任意工作台页面接收新呼叫,启用语音后使用浏览器本机中文 TTS 播报桌位及服务类型。
选择浏览器 SpeechSynthesis,前端无新依赖、无第三方密钥。云 TTS 需费用与联网,本地服务端 TTS 需额外模型服务;本期不接入。仅选择 localService=true 的中文语音,避免自动把业务内容交给远程语音服务。
- [x] 先编写新增任务识别、去重、排队、关闭和失败行为测试,观察失败后实施。
- [x] Pinia 统一后台任务数据与 WebSocket 推送监听;退出登录停止请求并清空语音。切换业务页面不中断监听。连接就绪与事件推送后补查任务,每 60 秒低频兜底同步。
- [x] 首次快照仅建立基线;后续出现的新 pending 任务按时间排序播报,已接单/完成的不播报。关闭语音期间持续建立已见基线,不在重新启用时重放。
- [x] 全局可见开启/关闭、试听、状态和错误提示;语音通过用户点击启用,刷新页面重新启用。播报不包含顾客备注。
- [x] 串行播放并限制队列;等待播报期间任务被接单或取消则移除。失败停止语音并提示重新开启,队列卡住有超时处理。
- [x] 执行自动化测试、类型检查、生产构建与浏览器检查,记录真实语音能力限制。
当前每个页面独立开启语音;同一店铺建议只在指定接单电脑开启,多个设备/标签都开启时各自播报。WebSocket 推送成功后立即查询并更新列表;断线按 1–30 秒退避重连,45 秒无心跳主动重连;浏览器后台冻结、设备睡眠或页面关闭时无法保证播报。恢复网络后的下一次成功同步播报仍待接单的新任务。首次进入的历史未接单任务继续在列表展示,不补播。
依据:[SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis)、[localService](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisVoice/localService)、[语音错误事件](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/error_event)。
WebSocket 协议:`GET /api/ws`,首帧 `{type:"auth",token}`,就绪 `ready`、通知 `tasks.changed`、每 20 秒 `heartbeat`;登录失效关闭 4403。token 不放 URL,Origin 校验且按门店隔离。服务端事务提交后通知,幂等重放不再次通知。首期单实例 Hub,未来多实例需 Redis Pub/Sub 分发。
## 接入位置
- `perler-beads-server/internal/server/websocket.go`:鉴权、按门店分组的连接、心跳与失效通知;`requests.go` 在创建/变更事务提交后发通知。
- `perler-beads-webui/src/notifications/store.ts`:Pinia 工作台级任务状态与同步;`realtime.ts`:连接状态、认证与退避重连。
- `core.ts`:快照去重与语音队列;`speech.ts`:本机中文 TTS 适配;`components/CallNotifications.vue`:全局控制与状态。
- `vite.config.ts` 与两份 Nginx 配置:WebSocket Upgrade 转发;frp HTTPS 透传无需增加独立隧道。
测试音与任务使用同一队列;不会因为试听而清空已有任务。服务端通知只带事件类型,不带顾客备注、图纸或用户资料,前端仍通过鉴权 API 获取当前数据。网络断线不重置任务已见集合,页面刷新则重新建立基线。
## 本次验收
- 后台 `npm test`:14 项通过;`npm run build`(包含 vue-tsc)通过。测试覆盖初始基线、重复/已处理任务、静音、中文本机音色、队列取消/过载/错误、试听期间的新任务、重连退避、鉴权失效与心跳超时。
- 服务端真实 PostgreSQL + Redis 的 `go test ./... -count=1`、`go test -race ./... -count=1`、`go vet ./...`、构建通过,详情见 [WS 服务端报告](ws-server-report.md)。
- 浏览器真实联调:已有登录工作台建立 WS,显示「实时连接正常」;选择系统本机音色「婷婷」,试听触发 onstart/onend;顾客 T01 桌提交呼叫后列表即时更新,最近播报变为「T01 桌,呼叫员工」。切换到图纸管理页后,再次提交补豆,最近播报变为「T01 桌,需要补充豆料」。这些是浏览器语音事件与界面验证,实际扬声器音量仍取决于设备设置。
- 两个联调呼叫已取消,保留验证记录;后台保持实时连接和语音开启,便于继续体验。页面刷新需要重新开启语音。
- 独立审查发现并修复了试听期间漏播/清空队列,以及 Nginx `$host` 丢失非标准端口导致 WS 同源校验失败。WS 路由使用 `$http_host` 保留端口。
- 未在 Docker/Nginx 实例、门店公网 TLS/frp、其他浏览器或睡眠唤醒设备上验收,部署前仍需测试。
截图:[新呼叫播报](screenshots/ws-tts-call.jpg)、[跨页面播报](screenshots/ws-tts-cross-page.jpg)。
+36
View File
@@ -0,0 +1,36 @@
# 店员后台交付报告
实现目录:`perler-beads-webui`。Vue 3 + TypeScript + Element Plus / icons + Pinia + Vue Router + Vite,CSS 原生样式,不含 Tailwind 或外部素材。森林绿与奶油色工作台,桌面侧栏与移动端折叠菜单。
## 已实现
- 管理员账号密码登录,不内置任何凭据。令牌存在 sessionStorage;每次受保护路由导航调用 `/api/me` 确認身份、员工角色与 store-001 门店,禁止仅凭令牌存在放行。401 未授权、403 未登录从业务 envelope 识别。403 清除会话并退出。
- 店内服务列表、数量摘要、状态/服务类型/桌码备注筛选、明确空/载入/错误状态、pending→accepted→completed 操作。10 秒轮询可暂停,隐藏页面不轮询,路由卸载停止计时并 AbortController 取消未完成请求。
- 图纸列表与检索、JSON 导入、名称分类编辑、发布/下架。仅管理员访问;私有图纸仍由服务端鉴权。导入尺寸、格子、色板、门店匹配由服务端权威校验。
- 色板与物料位置编辑抽屉,添加色号、颜色名称、HEX、可用状态;更新整门店色板。桌位新增/编辑、启停;门店信息与双 HTTPS 服务地址编辑。所有配置以当前授权门店提交,无跨店选择假授权。
- 图纸库与有 patternId 的服务任务可下载实际尺寸 SVG,带 Bearer 鉴权;识别下载接口 JSON 错误,释放 Object URL。服务端限制最多 200×200,SVG 是有界文件,客户端采用 Blob 下载;实际打印由店员手动完成。
- 支付与储值始终禁用,没有假支付、假充值或余额。
- 中文页面语言、内部后台 noindex、语义 heading/main/nav、跳转主要内容链接、控件标签、文字状态、键盘焦点、减少动画偏好、移动端抽屉与可横向滚动表格。
- 页面按路由懒加载,Element Plus 按实际组件注册,减少首屏 JS;无第三方字体/图片网络请求。
## 启动
```sh
cd perler-beads-webui
npm ci --cache /tmp/perler-npm-cache
npm run dev
```
开发服务监听 `127.0.0.1:5174`,`/api` 相对地址代理 `127.0.0.1:8080`。生产构建 `dist/`,部署需将 `/api` 代理服务端,并将前端路由回退至 index.html。管理员用户名与密码配置于服务端环境,后台手动输入。
本次本机联调账号:用户名 `admin`,密码 `local-integration-only`,门店 `store-001`。仅适用于本次启动的演示服务;自行部署按 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 配置。完整说明见 [README 的本地管理员说明](../README.md#当前本地演示管理员)。
## 验证
先写 envelope 权限错误与任务状态序列测试,执行出现 contract.ts 尚不存在的预期失败,再实现。
已执行通过:`npm test`(3 项,含真实 client 模拟 Fetch / Bearer / 过期会话校验)、`npm run build`(vue-tsc 类型检查 + Vite 生产构建)。安装审计 0 vulnerabilities。官方 npm 元数据确认框架版本。TypeScript latest(7)与当前 vue-tsc 不兼容,固定使用 TypeScript 5.9.3。依赖版本及 package-lock 保存。
最终首屏 JS gzip 146.84 kB,CSS gzip 50.13 kB,额外路由 chunk 每份约 1.3–3.2 kB gzip。构建无超大 chunk 警告。
浏览器桌面/移动交互与真实 API 联调由根任务统一验证;此报告当前不声称已完成该核查。没有 PostgreSQL/Redis 启动时,后台正确呈现连接失败。微信凭据、生产 DNS/TLS 和打印机硬件未验证。
+53
View File
@@ -0,0 +1,53 @@
# WebSocket 服务端交付报告
## 协议
新增 `GET /api/ws`。浏览器使用当前后台同源的 `wss://.../api/ws`;令牌不出现在 URL,任何 query string 均拒绝。Origin 必须精确匹配请求 Host(含端口),或来自 CORS_ORIGINS 明确配置的源;拒绝缺失、null、路径、userinfo 等异常 Origin。跨域后台必须把实际后台 HTTPS Origin 配入 CORS_ORIGINS。WebSocket 协议升级返回 101;Origin/升级错误允许真实 HTTP 错误状态,既有 JSON 契约保持原样。
客户端连接后 5 秒内发送首个文本 JSON:
```json
{"type":"auth","token":"后台登录返回的token"}
```
首帧与后续应用消息均限制 4096 字节。服务端直接查询 Redis 会话,必须有合法 user ID、storeId 与 admin/staff 角色;访客无法订阅。鉴权成功发送 `{"type":"ready"}`。服务请求新增和状态变更在 PostgreSQL 事务成功提交后,向对应 storeId 的员工连接发送 `{"type":"tasks.changed"}`。消息仅提示重新 GET /api/service-requests,没有任务正文、顾客信息、桌码或图纸内容。相同 requestId 重试、内容冲突和失败的状态变更均不广播。
每 20 秒服务端重验 Redis 会话,并发送 WebSocket ping 控制帧与 `{"type":"heartbeat"}`。浏览器协议栈自动回应 pong;前端无需发送应用层消息,也允许 `{"type":"pong"}`。heartbeat 用于前端判活,tasks.changed 用于刷新列表,再由前端选择播报。JSON 通知不是可靠事件队列;就绪与重连后必须主动拉取最新任务。
关闭码:4403 鉴权失败、5 秒首帧超时、会话过期/撤销或身份/门店变更;4400 非约定应用消息;1001 服务关闭;消息过大也可能由协议库以 1009 关闭。Redis 故障按失效会话关闭,客户端应重新验证登录并采用重连退避。
## 实现约束
- 连接读期限 45 秒,由协议 pong 延长;写操作最多 5 秒,Redis 查询为独立最多 3 秒 context。WS 升级请求不套用常规 HTTP 的 15 秒业务 context,因此连接可长期存活。
- 每个连接一个 reader 和一个 writer。应用 JSON、ping/pong 与正常关闭均通过 writer 发出;鉴权阶段尚未启动 writer,由握手处理同步完成。
- 单实例、按门店分组的轻量 hub。每连接容量 1 的变更通道合并失效通知,广播不等待浏览器写入,慢连接不会阻塞业务事务。控制 pong 使用容量 4 的独立小通道。
- 服务关闭显式通知已鉴权的 hijacked WS 连接,HTTP graceful shutdown 不依赖隐式关闭 WS。
- 当前广播是进程内的。未来多实例需要 Redis PubSub(门店主题)或可靠消息设施,否则其他实例上的连接不会收到更新。业务真源仍为 PostgreSQL。
Gorilla WebSocket 锁定 `v1.5.3`,使用前查询了[官方发布页](https://github.com/gorilla/websocket/releases)。该页当前标记 v1.5.3 为 latest;Go 官方数据库列出的弱 mask key 报告影响 v1.5.3 之前的版本。[Go 漏洞报告](https://pkg.go.dev/vuln/GO-2026-6278)
## 代理与构建
仅修改指定的 `deploy/nginx-https.example.conf` 和 `perler-beads-webui/nginx.conf`:新增精确 `/api/ws` 代理,HTTP/1.1 + Upgrade/Connection,75 秒读期限,10 秒发送期限,禁用代理缓冲。WS 路由使用 `Host $http_host` 保留原始 Host 与非标准端口(例如 Compose :8088),以便正确校验同源。生产 DNS、TLS、frp 和 Nginx 二进制未在本次 WS 测试运行,需在真实部署验证隧道。
新二进制:`/tmp/perler-ws-server`,主任务已接管启动本机 8080,并完成真实浏览器 WebSocket 与 TTS 联调。
## 真实执行验证
先写实际 Redis 鉴权测试,缺少端点时观察到预期失败:`websocket: bad handshake (status 200)`。新增/更新推送测试在接入提交钩子前观察到预期失败:`committed task did not push notification`,然后接入提交后广播。
使用真实本地 PostgreSQL 独立 `perler_test` 库(127.0.0.1:55432)与 Redis 专用测试 DB15(127.0.0.1:56379)。Redis session key 和 PostgreSQL fixture 使用随机 ID,仅删除测试自己创建的数据,不清空共享库。
实际执行通过:
```sh
export GOPATH=/tmp/perler-go GOCACHE=/tmp/perler-go-cache GOMODCACHE=/tmp/perler-go-mod
export TEST_DATABASE_URL='独立测试 PostgreSQL 连接串,数据库名需包含 test'
export TEST_REDIS_URL='redis://127.0.0.1:56379/15'
GIN_MODE=release go test ./... -count=1
GIN_MODE=release go test -race ./... -count=1
go vet ./...
go build -o /tmp/perler-ws-server ./cmd/server
```
普通测试约 22 秒,race 测试约 23 秒,均通过。覆盖管理员/员工认证、访客/无效令牌/空门店拒绝、同源/白名单/恶意 Origin、消息上限、未知协议、5 秒首帧超时、20 秒 heartbeat(证明超过 15 秒 HTTP context)、会话撤销关闭、真实事务提交后通知、跨门店不通知、幂等重放及冲突静默、越权状态变更静默、失效消息无私有字段,以及慢订阅者通知合并和广播不阻塞。原有 PostgreSQL 单连接池回归测试同时通过。