deliver Hongfa Laundry M1 desktop release

This commit is contained in:
manpengan
2026-04-23 16:50:24 +08:00
parent 20595a7545
commit 7deea00495
66 changed files with 19586 additions and 69 deletions

View File

@@ -0,0 +1,186 @@
# M1 Delivery Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Deliver `laundry-desk` M1 as a working Electron Windows NSIS app with receive, pickup, order list/detail, customer de-duplication, automatic/manual backup, type/lint/test gates, and CI artifact generation.
**Architecture:** Keep the approved Electron split: main owns SQLite and services, preload exposes a narrow typed API, renderer uses IPC only. Replace the current prototype DB/bootstrap with focused service modules, explicit Zod IPC schemas, Drizzle relations, and deterministic tests using in-memory SQLite. M2-M4 placeholders are removed or hidden unless needed by M1.
**Tech Stack:** Electron 41, React 19, TypeScript strict, Vite/electron-vite, better-sqlite3, Drizzle ORM, Zod, Vitest, Tailwind CSS, Playwright, electron-builder NSIS.
**Status 2026-04-23:** M1 local delivery completed by Codex. Windows x64 NSIS artifact generated at `dist/laundry-desk Setup 0.1.0.exe`; SHA256 stored next to it. Final gates passed: `npm run lint`, `npm run typecheck`, `npm test -- --run`, `npm run test:e2e`, `npm run build:win`, and `npm audit --omit=dev --audit-level=high` with no high-severity production findings. Remaining audit item is moderate `uuid` via `exceljs` / Tencent Cloud SDK; npm's automatic fix is breaking, so it is intentionally not applied for M1.
---
### Task 1: Build And Tooling Baseline
**Files:**
- Modify: `package.json`
- Modify: `tsconfig.node.json`
- Modify: `tsconfig.web.json`
- Modify: `electron.vite.config.ts`
- Create: `eslint.config.js`
- Modify: `vitest.config.ts`
- [ ] **Step 1: Run current failing gates**
Run: `npm run typecheck`, `npm run lint`, `npm test -- --run`
Expected: failures document the current baseline.
- [ ] **Step 2: Fix TypeScript project boundaries**
Set `rootDir` to `src` or use `rootDirs` so `src/shared` compiles for node and web. Keep `strict` inherited from electron-toolkit.
- [ ] **Step 3: Add working ESLint flat config**
Use TypeScript parser/config compatible with ESLint 8 and React JSX. Make the lint script run without "config not found".
- [ ] **Step 4: Rebuild native dependencies for local Node if needed**
Run: `npm rebuild better-sqlite3`.
Expected: Vitest can import `better-sqlite3`.
- [ ] **Step 5: Re-run gates**
Run: `npm run typecheck`, `npm run lint`, `npm test -- --run`
Expected: tooling errors are resolved or narrowed to real implementation failures.
### Task 2: Database And Service Core
**Files:**
- Modify: `src/main/db/schema.ts`
- Modify: `src/main/db/index.ts`
- Create: `src/main/db/migrate.ts`
- Create: `src/main/db/client.ts`
- Modify: `src/main/services/customerService.ts`
- Modify: `src/main/services/orderService.ts`
- Modify: `src/main/services/pickupCodeService.ts`
- Modify: `src/main/services/auditService.ts`
- Modify: `tests/unit/services.test.ts`
- [ ] **Step 1: Write service tests for M1 behavior**
Cover customer upsert by phone, order creation transaction, integer cents validation, order number sequencing, pickup code format, pickup search by code/phone/order number, pickup balance settlement, and audit log creation.
- [ ] **Step 2: Verify tests fail for missing/incorrect behavior**
Run: `npm test -- --run tests/unit/services.test.ts`
Expected: failures for missing search, transaction/audit, validation, or schema gaps.
- [ ] **Step 3: Implement DB initialization**
Expose a `createDbClient(sqlite)` helper for tests and `getDb()` for app runtime. Enable WAL and foreign keys. Run idempotent SQL migrations for all M1 tables and indexes from one source.
- [ ] **Step 4: Implement service behavior**
Use transactions for create order and pickup. Generate `order_no` and `pickup_code` inside the transaction with unique-conflict retry. Recompute totals from item rows, require integer cents, require `paidAmount <= totalAmount`, and write audit rows through the same transaction.
- [ ] **Step 5: Re-run unit tests**
Run: `npm test -- --run tests/unit/services.test.ts`
Expected: service tests pass.
### Task 3: IPC Contract And Preload API
**Files:**
- Modify: `src/shared/index.ts`
- Modify: `src/shared/schemas.ts`
- Modify: `src/preload/index.ts`
- Modify: `src/main/ipc/orders.ts`
- Modify: `src/main/ipc/customers.ts`
- Modify: `src/main/ipc/settings.ts`
- Create: `src/main/ipc/backup.ts`
- Modify: `src/main/index.ts`
- [ ] **Step 1: Write IPC schema tests where practical**
Cover invalid order id, invalid search query, invalid settings key, and invalid backup action schemas.
- [ ] **Step 2: Implement uniform IPC helpers**
Every handler returns `{ ok: true, data } | { ok: false, error }`, logs detailed errors in main, and returns user-safe messages.
- [ ] **Step 3: Narrow preload exposure**
Expose only `window.api`; remove default `window.electron`; use typed request/response signatures instead of `any`.
- [ ] **Step 4: Add CSP and production-safe window behavior**
Remove debug background, `alwaysOnTop`, devtools auto-open, and fixed startup delay. Add CSP to renderer HTML.
- [ ] **Step 5: Re-run typecheck/lint**
Run: `npm run typecheck && npm run lint`
Expected: zero errors.
### Task 4: Renderer M1 Flows
**Files:**
- Modify: `src/renderer/index.html`
- Modify: `src/renderer/src/assets/main.css`
- Modify: `src/renderer/src/env.d.ts`
- Modify: `src/renderer/src/routes/index.tsx`
- Modify: `src/renderer/src/components/Layout.tsx`
- Modify: `src/renderer/src/routes/Home.tsx`
- Modify: `src/renderer/src/routes/Receive.tsx`
- Modify: `src/renderer/src/routes/Pickup.tsx`
- Modify: `src/renderer/src/routes/Orders.tsx`
- Modify: `src/renderer/src/routes/OrderDetail.tsx`
- Modify: `src/renderer/src/routes/Customers.tsx`
- Modify: `src/renderer/src/routes/Settings.tsx`
- [ ] **Step 1: Add focused renderer tests if test harness exists, otherwise rely on typecheck and manual/E2E smoke**
Validate route imports compile and typed IPC calls are correct.
- [ ] **Step 2: Restore usable styling**
Either configure Tailwind correctly or replace with a compact CSS utility layer sufficient for the current class usage. Preserve Apple HIG-inspired layout and responsive behavior.
- [ ] **Step 3: Complete receive flow**
Phone lookup, customer upsert, item rows with cents conversion, partial payment selection, validation messages, success reset, and navigation to order detail.
- [ ] **Step 4: Complete pickup flow**
Search by pickup code, phone, order number, or customer name; render results; support balance settlement and picked-up transition.
- [ ] **Step 5: Complete list/detail/settings M1 routes**
Order list pagination basics, order detail with print button hidden/disabled until M3, customers list/search, settings shop name, manual backup, and backup list/restore placeholder only if clearly marked out-of-scope.
### Task 5: Backup And Delivery Verification
**Files:**
- Modify: `src/main/services/backupService.ts`
- Modify: `src/main/ipc/backup.ts`
- Modify: `tests/unit/services.test.ts`
- Modify: `.github/workflows/build.yml`
- Modify: `README.md`
- Create: `docs/CHANGELOG.md`
- [ ] **Step 1: Add backup tests**
Use a temp directory and a test database. Verify backup zip is created atomically, contains `laundry.db`, and rotate keeps 30 newest files.
- [ ] **Step 2: Implement backup service**
Checkpoint WAL, write to temp zip, finalize, rename atomically, rotate after success, and expose manual backup IPC.
- [ ] **Step 3: Add CI gates**
Run install, typecheck, lint, test, build, and `build:win`. Upload NSIS artifact and SHA256.
- [ ] **Step 4: Run local verification**
Run: `npm run typecheck`, `npm run lint`, `npm test -- --run`, `npm run build`
Expected: all pass locally.
- [ ] **Step 5: Attempt package verification**
On macOS run `npm run build:unpack` if supported. Final `.exe` must be generated by GitHub Actions `windows-latest`.

View File

@@ -16,6 +16,7 @@ authors: claude (brainstorm), manpengan (decision)
- 数据:本地 SQLiteWAL + 每日自动备份)
**典型场景:**
- 收件:查回头客 → 录物品明细 → 计价收款 → 生成 4 位取件码 → 打印登记单
- 取件:客户报取件码 / 电话 / 单号 → 查询 → 收尾款 → 标记已取 → 打印取件条
- 管理:店主查看日/月营业额、逾期未取、回头率
@@ -28,25 +29,25 @@ authors: claude (brainstorm), manpengan (decision)
## 3. 技术栈
| 层 | 选型 |
|---|---|
| 应用框架 | Electron 32+ |
| 前端 | React 19 + TypeScript 5 (strict) |
| UI | Tailwind CSS 4 + shadcn/ui |
| 动效 | Framer Motion 11 |
| 字体 | SF Pro Display/Text + PingFang SC |
| 状态 | Zustand |
| 路由 | React Router 7 |
| 数据库 | better-sqlite3 + Drizzle ORM |
| 密码哈希 | @node-rs/argon2 |
| 构建 | Vite + electron-vite |
| 打包 | electron-builderNSIS |
| 图表 | Recharts |
| 打印 | electron-pos-printer58mm ESC/POS |
| 短信 | @tencentcloud/tencentcloud-sdk-nodejs-sms |
| Excel | exceljs |
| 测试 | Vitest单测+ PlaywrightE2E |
| CI | GitHub Actions`windows-latest` 构建 + artifact |
| 层 | 选型 |
| -------- | -------------------------------------------------- |
| 应用框架 | Electron 32+ |
| 前端 | React 19 + TypeScript 5 (strict) |
| UI | Tailwind CSS 4 + shadcn/ui |
| 动效 | Framer Motion 11 |
| 字体 | SF Pro Display/Text + PingFang SC |
| 状态 | Zustand |
| 路由 | React Router 7 |
| 数据库 | better-sqlite3 + Drizzle ORM |
| 密码哈希 | @node-rs/argon2 |
| 构建 | Vite + electron-vite |
| 打包 | electron-builderNSIS |
| 图表 | Recharts |
| 打印 | electron-pos-printer58mm ESC/POS |
| 短信 | @tencentcloud/tencentcloud-sdk-nodejs-sms |
| Excel | exceljs |
| 测试 | Vitest单测+ PlaywrightE2E |
| CI | GitHub Actions`windows-latest` 构建 + artifact |
**UI 视觉规范Apple HIG** SF Pro + PingFang SC、圆角 1216px、半透明/毛玻璃卡片、macOS System Colors`systemBlue #007AFF`、Framer Motion 过渡、深色模式跟随系统、窗口 `titleBarStyle: 'hiddenInset'`Mac 开发态)/ Windows 下降级为圆角无边框。
@@ -54,16 +55,16 @@ authors: claude (brainstorm), manpengan (decision)
金额一律用**整数分**存储(杜绝浮点)。
| 表 | 关键字段 |
|---|---|
| `customers` | id, name, phone (unique), vip_level, total_orders, total_spent, created_at, updated_at |
| `orders` | id, order_no (unique, `YYYYMMDD-NNNN`), pickup_code (char(4)), customer_id (fk), status (pending/ready/picked_up/cancelled), total_amount, paid_amount, payment_method (cash/wechat/alipay/card/unpaid), receive_date, expected_pickup_date, actual_pickup_at, staff_id (fk), picked_up_by (fk nullable), notes, created_at, updated_at |
| `order_items` | id, order_id (fk cascade), item_type, service_type (wash/dry_clean/iron), quantity, unit_price, subtotal, item_notes |
| `order_photos` | id, order_id (fk cascade), file_path (相对 userData), taken_at |
| `staffs` | id, username (unique), password_hash (Argon2), display_name, role (admin/staff), is_active, created_at, last_login_at |
| `sms_log` | id, order_id (fk), phone, content, status (pending/sent/failed), provider_response (json), sent_at |
| `settings` | key (pk), value (json), updated_at |
| `audit_log` | id, staff_id (fk nullable), action (create/update/delete/pickup/cancel/login/export), entity, entity_id, diff (json), created_at |
| 表 | 关键字段 |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customers` | id, name, phone (unique), vip_level, total_orders, total_spent, created_at, updated_at |
| `orders` | id, order_no (unique, `YYYYMMDD-NNNN`), pickup_code (char(4)), customer_id (fk), status (pending/ready/picked_up/cancelled), total_amount, paid_amount, payment_method (cash/wechat/alipay/card/unpaid), receive_date, expected_pickup_date, actual_pickup_at, staff_id (fk), picked_up_by (fk nullable), notes, created_at, updated_at |
| `order_items` | id, order_id (fk cascade), item_type, service_type (wash/dry_clean/iron), quantity, unit_price, subtotal, item_notes |
| `order_photos` | id, order_id (fk cascade), file_path (相对 userData), taken_at |
| `staffs` | id, username (unique), password_hash (Argon2), display_name, role (admin/staff), is_active, created_at, last_login_at |
| `sms_log` | id, order_id (fk), phone, content, status (pending/sent/failed), provider_response (json), sent_at |
| `settings` | key (pk), value (json), updated_at |
| `audit_log` | id, staff_id (fk nullable), action (create/update/delete/pickup/cancel/login/export), entity, entity_id, diff (json), created_at |
**索引:** `orders(pickup_code)``orders(customer_id, status, receive_date)``audit_log(created_at)``customers(phone) unique``orders(order_no) unique`
@@ -97,6 +98,7 @@ src/
```
**硬约束:**
- `contextIsolation: true` / `nodeIntegration: false` / `sandbox: true` / CSP 严格
- Renderer 零 Node/DB 访问,全走 IPC
- IPC 命名 `<domain>:<action>`(如 `orders:create`);入参一律过 Zod返回 `{ ok: true, data } | { ok: false, error: { code, message } }`
@@ -106,25 +108,29 @@ src/
## 6. 核心工作流
### 6.1 收件
电话查回头客 → 明细多行 → 付款方式 + 实收 → 事务生成 `pickup_code` + `order_no` → 写 `orders` + `order_items`+ 可选 `order_photos`)→ 更新 `customers.total_orders/total_spent``audit_log`M3打印登记单。
### 6.2 取件
取件码(优先)/ 电话 / 单号 / 姓名查询 → 多条时列表选 → 有欠款先收 → `status = picked_up``actual_pickup_at``picked_up_by``audit_log`M3打印取件条。
### 6.3 自动备份
`app.ready` 启动 `node-cron` 03:00 → `PRAGMA wal_checkpoint(TRUNCATE)` → 复制 `.db` → zip → 滚动保留 30 份。
### 6.4 短信通知M4
手动/批量触发 → Service 调腾讯云 SMS SDK → 写 `sms_log`pending → sent/failed→ 状态回填订单详情。
## 7. 分期路线图
| 期 | Tag | 交付 |
|---|---|---|
| **M1** | `v0.1.0` | 项目骨架 + Apple HIG 设计系统 + 收件/取件/列表/详情 + 客户自动去重 + 自动备份 + Windows NSIS 打包 |
| **M2** | `v0.2.0` | 价格模板 + 按件计费 + 折扣 + 付款/欠款 + 日/月报表Recharts+ 逾期未取 + Excel 导入导出 |
| **M3** | `v0.3.0` | 收件拍照13 张,存 `userData/photos/YYYY-MM/`+ 58mm 热敏打印登记单 / 取件条 |
| **M4** | `v0.4.0``v1.0.0` | 登录Argon2+ 权限admin/staff+ 审计日志全面绑定 + 腾讯云 SMS 可取件通知(可关闭) |
| 期 | Tag | 交付 |
| ------ | ------------------- | ------------------------------------------------------------------------------------------------- |
| **M1** | `v0.1.0` | 项目骨架 + Apple HIG 设计系统 + 收件/取件/列表/详情 + 客户自动去重 + 自动备份 + Windows NSIS 打包 |
| **M2** | `v0.2.0` | 价格模板 + 按件计费 + 折扣 + 付款/欠款 + 日/月报表Recharts+ 逾期未取 + Excel 导入导出 |
| **M3** | `v0.3.0` | 收件拍照13 张,存 `userData/photos/YYYY-MM/`+ 58mm 热敏打印登记单 / 取件条 |
| **M4** | `v0.4.0``v1.0.0` | 登录Argon2+ 权限admin/staff+ 审计日志全面绑定 + 腾讯云 SMS 可取件通知(可关闭) |
## 8. 验收门禁(每期必过)
@@ -135,25 +141,25 @@ src/
## 9. 分工
| 角色 | 职责 | 工具 |
|---|---|---|
| 角色 | 职责 | 工具 |
| --------------------- | ------------------------------------------ | ----------- |
| **Claude (Opus 4.7)** | brainstorm / spec / 门禁验收 / code review | Claude Code |
| **Codex** | 关键节点二次审查(架构 / 安全 / 并发) | Codex CLI |
| **Gemini** | M1~M4 主力实现、测试编写、修 build | Gemini CLI |
| **manpengan** | 决策 / UI 走查 / 发版 | — |
| **Codex** | 关键节点二次审查(架构 / 安全 / 并发) | Codex CLI |
| **Gemini** | M1~M4 主力实现、测试编写、修 build | Gemini CLI |
| **manpengan** | 决策 / UI 走查 / 发版 | — |
**节奏:** 每期开始 Claude 在 milestone issue 补实施细节 → Gemini 提 PR → Claude 审 → Codex 关键点复审 → Claude 过门禁清单 → manpengan tag release。
## 10. 风险与缓解
| 风险 | 缓解 |
|---|---|
| 风险 | 缓解 |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Mac 交叉打 Windows exebetter-sqlite3 native 模块失败 | GH Actions `windows-latest` 为准;本地 `pnpm rebuild`electron-builder `buildDependenciesFromSource: false` |
| 打印机型号差异 | M3 抽象 `PrinterDriver` 接口,先支持 58mm 通用 ESC/POS型号配置化 |
| 腾讯云 SMS 模板审核周期 | Provider 可替换;模板 id 从 settings 读;无短信时仍可发版 |
| SQLite 单文件损坏 | WAL + 每日 zip + 备份校验任务 |
| Electron XSS → 任意代码 | contextIsolation + sandbox + CSP + 所有 IPC 过 Zod |
| 密钥泄漏(短信 SecretKey | M4 用 OS keychain`keytar`)加密存储,非明文入库 |
| 打印机型号差异 | M3 抽象 `PrinterDriver` 接口,先支持 58mm 通用 ESC/POS型号配置化 |
| 腾讯云 SMS 模板审核周期 | Provider 可替换;模板 id 从 settings 读;无短信时仍可发版 |
| SQLite 单文件损坏 | WAL + 每日 zip + 备份校验任务 |
| Electron XSS → 任意代码 | contextIsolation + sandbox + CSP + 所有 IPC 过 Zod |
| 密钥泄漏(短信 SecretKey | M4 用 OS keychain`keytar`)加密存储,非明文入库 |
## 11. 假设