diff --git a/.cursor/rules/backend-nestjs.mdc b/.cursor/rules/backend-nestjs.mdc new file mode 100644 index 0000000..2e1ade5 --- /dev/null +++ b/.cursor/rules/backend-nestjs.mdc @@ -0,0 +1,51 @@ +--- +description: 杜康好客 NestJS 后端编码规范 +globs: server/**/*.ts +alwaysApply: false +--- + +# 后端规范 + +## 模块目录 + +``` +server/dukang-api/src/ +├── modules/{iam,trade,benefit,catalog,store,redeem,settlement,ops,analytics,health}/ +├── common/{guards,decorators,interceptors,filters,prisma,redis}/ +├── integrations/{pay,sms,delivery}/ +└── jobs/ +``` + +## 新接口流程 + +1. 在 **OWNER 模块** 建 `dto/`、`*.service.ts`、`*.controller.ts` +2. Service 只写本模块 Prisma 表 +3. 跨域 inject 其他 Module 的 exported Service +4. Controller 加对应 Guard(`JwtAuthGuard`、`PhoneVerifiedGuard` 等) +5. 响应 `{ code: 0, message: 'ok', data }` +6. 同步 `packages/shared-types` + +## Controller 示例 + +```typescript +@Controller('trade/orders') +@UseGuards(JwtAuthGuard) +export class TradeController { + constructor(private readonly tradeService: TradeService) {} +} +``` + +## 鉴权路由前缀 + +| 端 | 前缀 | +|----|------| +| C 端 | `/auth`, `/user`, `/catalog`, `/trade`, `/benefit`, `/redeem`, `/stores` | +| 门店 | `/shop/auth`, `/shop/redeem`, `/shop/store` | +| 合伙人 | `/partner/*` | +| 总部 | `/admin/*` | + +## 数据约定 + +- 埋点 → `log_user_analytics`;业务审计 → `common_event` +- 支付 → `log_third_party` + `user_order.pay_*`(无 payments 表) +- 核销码 → Redis 5min(无 DB 表) diff --git a/.cursor/rules/dukang-core.mdc b/.cursor/rules/dukang-core.mdc index 16eee4f..070ea54 100644 --- a/.cursor/rules/dukang-core.mdc +++ b/.cursor/rules/dukang-core.mdc @@ -1,16 +1,27 @@ --- -description: 杜康好客全局约束 — 文档优先级、OWNER 边界、禁止跨模块直写表 +description: 杜康好客全局约束 — Monorepo 布局、文档优先级、2 人 OWNER 边界 alwaysApply: true --- # 杜康好客 · 核心规则 +## Monorepo 布局 + +- `apps/` — preV1:`h5-user`、`h5-shop`、`h5-partner`、`admin-web` +- `packages/` — `shared-types`、`domain`、`shared-ui` +- `server/dukang-api/` — NestJS 单体 API +- `pages/` — UI 原型(**只读参照**) + +> V2 目标为 Taro 四端(`mini-user` 等),编码以 **当前实际目录** 为准。 + ## 文档 - 业务事实源:`杜康好客-V2编码手册.md` - preV1 裁剪:`杜康好客-preV1编码手册.md` - 协作:`conventions.md` · AI 入口:`AGENTS.md` +**禁止**:编码时修改业务规则;需求变更只改 V2 手册。 + ## 边界(2 人团队) | 负责人 | 路径 | @@ -19,13 +30,20 @@ alwaysApply: true | 刘京尧 | `apps/h5-partner/`, `apps/h5-shop/`, `modules/{store,redeem}/` | - 跨模块只 inject **exported Service**,禁止 `prisma` 写他人表 -- `apps/*` 禁止 import `server/*` +- `apps/*` 禁止 import `server/*` 或其他 app - Mock 只在 `integrations/*`,不在业务 Service 散落 ## 共享契约 - 枚举/DTO → `packages/shared-types` -- 纯规则 → `packages/domain` +- 纯规则 → `packages/domain`(无 IO) + +## 核心业务常量 + +- 权益额 = `benefit_amount ?? price` +- 核销:0 < amount ≤ min(balance, **500**) +- 订单 Tab:`all | pending_pay | pending_ship | pending_receive | completed` +- API:`/api/v1`,响应 `{ code, message, data }` ## 提交 diff --git a/.cursor/rules/frontend-apps.mdc b/.cursor/rules/frontend-apps.mdc deleted file mode 100644 index b2438b0..0000000 --- a/.cursor/rules/frontend-apps.mdc +++ /dev/null @@ -1,34 +0,0 @@ ---- -description: 三端 H5 + admin-web 前端边界与 API 请求规范 -globs: apps/**/*.{ts,tsx} -alwaysApply: false ---- - -# 前端 App 规则 - -## 隔离 - -- 禁止 import 其他 `apps/*` 或 `server/*` -- 类型从 `@dukang/shared-types`;组件从 `@dukang/shared-ui` - -## 请求 - -- Base: `/api/v1` -- Headers: `Authorization`, `X-Client-App`(USER_H5 | SHOP_H5 | PARTNER_H5) - -## UI 约束 - -- C 端订单 **5 Tab**(含 pending_ship) -- 门店列表仅展示 `OPEN` 状态 -- 原型 `pages/` 只读参照,不在此目录改业务代码 - -## App 归属(2 人团队) - -| App | 负责人 | 勿改他端页面 | -|-----|--------|--------------| -| h5-user | jacy-dukang | shop 核销、partner 录店 | -| h5-shop | 刘京尧 | user 出码、admin 报表 | -| h5-partner | 刘京尧 | user 下单、admin 开城 | -| admin-web | jacy-dukang | 非 V2 小程序规格 | - -路由对照:`pages/ROUTE_MAP.md` diff --git a/.cursor/rules/frontend-h5.mdc b/.cursor/rules/frontend-h5.mdc new file mode 100644 index 0000000..0415a19 --- /dev/null +++ b/.cursor/rules/frontend-h5.mdc @@ -0,0 +1,53 @@ +--- +description: 杜康好客前端 H5 / Admin 编码规范与 OWNER 边界 +globs: apps/**/*.{tsx,ts} +alwaysApply: false +--- + +# 前端规范 + +## 目录约定 + +``` +src/ +├── components/ # PascalCase(AppToast.tsx) +├── pages/ # *Page.tsx +├── layouts/ # *Layout.tsx +├── contexts/ # *Context.tsx +├── lib/ # kebab-case(api.ts, client-location.ts) +├── App.tsx +└── main.tsx +``` + +## 四 App + +| App | 端口 | 负责人 | 样式 | X-Client-App | +|-----|------|--------|------|--------------| +| h5-user | 5173 | jacy-dukang | shared-ui tokens | USER_H5 | +| h5-shop | 5174 | 刘京尧 | shared-ui tokens | SHOP_H5 | +| h5-partner | 5175 | 刘京尧 | shared-ui tokens | PARTNER_H5 | +| admin-web | 5175 | jacy-dukang | Ant Design 5 | HQ_WEB | + +> h5-partner 与 admin-web 勿同时 dev。H5 三端用 `@dukang/shared-ui`;admin-web 独立 Ant Design。 + +## 隔离与请求 + +- 禁止 import 其他 `apps/*` 或 `server/*` +- 类型从 `@dukang/shared-types` import +- Base: `/api/v1`;Headers: `Authorization` + `X-Client-App` +- Vite dev 代理 `/api` → `localhost:3000` + +## UI 约束 + +- C 端订单 **5 Tab**(含 pending_ship) +- 门店列表仅 `OPEN` 状态 +- 原型 `pages/` 只读;路由对照 `pages/ROUTE_MAP.md` + +## App 归属(勿改他端) + +| App | 勿改 | +|-----|------| +| h5-user | shop 核销、partner 录店 | +| h5-shop | user 出码、admin 报表 | +| h5-partner | user 下单、admin 开城 | +| admin-web | 非 V2 小程序规格 | diff --git a/.cursor/rules/git-conventions.mdc b/.cursor/rules/git-conventions.mdc new file mode 100644 index 0000000..0524e85 --- /dev/null +++ b/.cursor/rules/git-conventions.mdc @@ -0,0 +1,32 @@ +--- +description: Git 提交与 PR 协作规范 +alwaysApply: true +--- + +# Git 规范 + +## 提交格式 + +Conventional Commits,scope = 端或模块名: + +``` +feat(trade): add order preview API +fix(redeem): enforce 500 yuan cap +chore(shared-types): add OrderStatus enum +``` + +## 禁止提交 + +- `.env`、`.env.local` 等敏感配置 +- `node_modules/`、`dist/`(已在 .gitignore) + +## PR 与 Review + +- feature 分支 + 小步 PR +- 跨模块改动需相关双 Owner Review +- Prisma 迁移必须多 Owner Review + +## Agent 行为 + +- **仅用户明确要求时** 才执行 git commit / push +- 不 amend 已推送的 commit,不 force push main/master diff --git a/.cursor/rules/packages-shared.mdc b/.cursor/rules/packages-shared.mdc index 32fad2a..ab73ae1 100644 --- a/.cursor/rules/packages-shared.mdc +++ b/.cursor/rules/packages-shared.mdc @@ -1,16 +1,25 @@ --- -description: packages 共享契约 — shared-types 与 domain 变更策略 -globs: packages/**/* +description: 杜康好客共享包(shared-types / domain / shared-ui)规范 +globs: packages/**/*.{ts,tsx} alwaysApply: false --- -# packages 规则 +# 共享包规范 + +## 包职责 + +| 包 | 用途 | 约束 | +|----|------|------| +| `@dukang/shared-types` | DTO、枚举、错误码、JWT Payload | 前后端唯一契约 | +| `@dukang/domain` | 纯函数业务规则 | **禁止 IO**,含 Vitest 单测 | +| `@dukang/shared-ui` | H5 共享组件 + `tokens.css` | 仅 H5 三端使用 | ## shared-types -- 前后端唯一 DTO/枚举来源 +- 枚举用 `SCREAMING_SNAKE_CASE`(如 `OrderStatus.PENDING_PAY`) +- API Tab 查询用 snake_case(`pending_pay`) - **加法优先**:新字段 optional;删除/改名先 deprecated -- 改 API 必须同步 V2 手册 §六 +- 改 API 必须同步 V2 手册 §六 与 shared-types ## domain @@ -18,11 +27,17 @@ alwaysApply: false - 起购 2/6 瓶、权益 `benefitAmount ?? price`、核销 ¥500 上限在此实现 - 变更必须有单元测试 +```typescript +const benefitAmount = product.benefitAmount ?? product.price; +// 同城起购 2 瓶 / 跨城 6 瓶 +// 核销上限 ¥500 +``` + ## shared-ui - 跨 App 组件;禁止 import 特定 App 代码 -- 设计令牌与 preV1 三 H5 共用 +- admin-web 使用 Ant Design,不引用 shared-ui ## 破坏性改动 -需通知 jacy-dukang 与 刘京尧(若影响 partner/shop API)并在 PR 说明影响面。 +需通知 **jacy-dukang** 与 **刘京尧**(若影响 partner/shop API)并在 PR 说明影响面。Prisma 迁移需双方 Review(涉及 store/redeem 表时)。 diff --git a/.cursor/skills/dukang-coding/SKILL.md b/.cursor/skills/dukang-coding/SKILL.md index 1628e5d..691858a 100644 --- a/.cursor/skills/dukang-coding/SKILL.md +++ b/.cursor/skills/dukang-coding/SKILL.md @@ -1,9 +1,10 @@ --- name: dukang-coding description: >- - Implements Dukang Haoke (杜康好客) features following V2/preV1 coding manuals, - module OWNER boundaries, and shared-types contracts. Use when coding APIs, - pages, Prisma logic, task cards, or fixing bugs in this monorepo. + Implements 杜康好客 features following V2/preV1 coding manuals, module OWNER + boundaries, and shared-types contracts. Use when coding APIs, pages, Prisma logic, + task cards, or fixing bugs — not for PRD/requirements review (use @dukang-project). +disable-model-invocation: true --- # 杜康好客 · 编码 Skill @@ -11,8 +12,11 @@ description: >- ## 何时启用 - 实现/修复 `apps/*`、`server/dukang-api`、`packages/*` 功能 -- 用户给出任务卡 ID 或 @ 本 skill -- **不要**用于纯文档问答(直接读手册即可) +- 实现任务卡(`M2-BE-TRD-002`、`P1-M2-002` 等) +- 用户给出任务卡 ID、说「开始 coding」或 @dukang-coding +- 修复 V1 范围 Bug + +**不用于**:写 PRD、评审需求、纯文档问答 → 用 @dukang-project 或直接读手册 ## 会话启动(按序) @@ -22,10 +26,13 @@ description: >- 4. UI 任务 → `pages/ROUTE_MAP.md` + 对应 `pages/{端}/` 原型 5. 编码前过一遍下方 Checklist +**需求变更** → 只改 V2 手册,编码时不改业务规则。 + ## 编码前 Checklist ``` - [ ] 任务所属 OWNER 与目标路径已确认 +- [ ] 已读手册 PRD 对应 § 与 pages/ 原型 - [ ] API 路径、Guard、表名已从 V2 §六/§五 核对 - [ ] 跨模块需求 → 只 inject 对方 exported Service - [ ] 新枚举/DTO → packages/shared-types @@ -48,9 +55,9 @@ Workflow:DTO → Service(本模块表)→ Controller + Guard → 同步 sh 跨模块示例(允许): ```typescript -// trade.service.ts — OWNER A +// trade.service.ts — jacy-dukang constructor( - private readonly benefitService: BenefitService, // from BenefitModule + private readonly benefitService: BenefitService, private readonly catalogService: CatalogService, ) {} await this.benefitService.grantOnOrderPaid(orderId); @@ -68,7 +75,14 @@ await this.prisma.order.update({ ... }); 1. 路由对照 `pages/ROUTE_MAP.md` 2. `src/lib/api.ts` 统一 fetch + `X-Client-App` 3. 类型从 `@dukang/shared-types` import -4. C 端订单 Tab:`all | pending_pay | pending_ship | pending_receive | completed` +4. preV1 App:`h5-user` / `h5-shop` / `h5-partner` / `admin-web` +5. C 端订单 Tab:`all | pending_pay | pending_ship | pending_receive | completed` + +## 数据库变更 + +1. 改 `server/dukang-api/prisma/schema.prisma` 对齐手册 §五 +2. 迁移需 OWNER Review(store/redeem 表 → @刘京尧) +3. 初始化 SQL:`server/dukang-api/prisma/init_v3.sql` ## 核心业务(packages/domain) @@ -76,6 +90,10 @@ await this.prisma.order.update({ ... }); const benefitAmount = product.benefitAmount ?? product.price; // 核销:0 < amount ≤ min(balance, 500) // 同城 min 2 瓶 / 跨城 min 6 瓶 + +// 支付成功 → log_third_party + user_order.pay_status=PAID +// → user_benefit_coupon + common_event(BENEFIT_LEDGER, GRANT) +// 埋点 → log_user_analytics;业务审计 → common_event ``` ## 核销并发(M3+) @@ -92,10 +110,13 @@ const benefitAmount = product.benefitAmount ?? product.price; - 任务卡验收项全部满足 - 无跨模块 Prisma 直写 -- `pnpm lint` 无新增错误;domain 相关单测通过 +- `packages/domain` 相关单测通过 +- `pnpm lint` 无新增错误 +- 改了 API/表 → 同步手册与 shared-types ## 延伸阅读 -- 模块/表/依赖矩阵:[reference-backend.md](reference-backend.md) +- 模块/表/路由:[reference-backend.md](reference-backend.md) - 页面↔API 速查:[reference-frontend.md](reference-frontend.md) -- preV1 Mock 细节:`dukang-prev1` skill +- preV1 Mock:`dukang-prev1` skill +- 详细清单:根目录 `skills.md` diff --git a/.cursor/skills/dukang-coding/reference-backend.md b/.cursor/skills/dukang-coding/reference-backend.md index 31a0354..c9e5948 100644 --- a/.cursor/skills/dukang-coding/reference-backend.md +++ b/.cursor/skills/dukang-coding/reference-backend.md @@ -7,6 +7,7 @@ - Prefix: `/api/v1` - Response: `{ code: 0, message: 'ok', data }` - JWT: `actorType` + `actorId` + `clientApp` +- 请求头:`Authorization`、`X-Client-App` ## clientApp → actorType → 表 @@ -15,36 +16,110 @@ | USER_H5 | USER_MINI | USER | user_user | | SHOP_H5 | SHOP_H5 | STORE | store_account | | PARTNER_H5 | PARTNER_MINI | PARTNER | partner_account | -| — | HQ_MINI | HQ | hq_account | +| HQ_WEB | HQ_MINI | HQ | hq_account | + +## Guard 对照 + +| Guard | 用途 | +|-------|------| +| JwtAuthGuard | 需登录 | +| PhoneVerifiedGuard | C 端需绑定手机(下单/支付) | +| OptionalJwtAuthGuard | 可选登录(bootstrap) | + +Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。 ## Module → API 前缀 -| Module | 前缀示例 | OWNER | -|--------|----------|-------| -| iam | `/auth`, `/user` | A | -| catalog | `/catalog`, `/admin/cities`, `/admin/products` | C | -| trade | `/trade`, `/partner/orders`, `/admin/orders` | A | -| benefit | `/benefit` | A | -| store | `/stores`, `/partner/stores`, `/admin/store-audits` | B | -| redeem | `/redeem`, `/shop/redeem` | D | -| settlement | `/settlement`, `/partner/settlement`, `/admin/settlement` | C | -| ops | `/admin/dashboard`, `/admin/reports` | C | -| analytics | `/analytics`, `/promo/touch` | A | -| callbacks | `/callbacks/wechat/pay`, `/callbacks/xfx/delivery` | Lead | +| Module | 前缀示例 | 负责人 | +|--------|----------|--------| +| iam | `/auth`, `/user` | jacy-dukang | +| catalog | `/catalog`, `/admin/cities`, `/admin/products` | jacy-dukang | +| trade | `/trade`, `/partner/orders`, `/admin/orders` | jacy-dukang | +| benefit | `/benefit` | jacy-dukang | +| store | `/stores`, `/partner/stores`, `/admin/store-audits` | 刘京尧 | +| redeem | `/redeem`, `/shop/redeem` | 刘京尧 | +| settlement | `/settlement`, `/partner/settlement`, `/admin/settlement` | jacy-dukang | +| ops | `/admin/dashboard`, `/admin/reports` | jacy-dukang | +| analytics | `/analytics`, `/promo/touch` | jacy-dukang | +| callbacks | `/callbacks/wechat/pay`, `/callbacks/xfx/delivery` | jacy-dukang | ## 表前缀 → Module 写 OWNER -| 前缀 | 示例表 | OWNER Module | -|------|--------|--------------| -| user_ | user_user, user_order, user_benefit_coupon | iam / trade / benefit | -| store_ | store_store, store_account, store_payout | store / iam / settlement | -| partner_ | partner_partner, partner_bill | store(B) / settlement(C) | -| hq_ | hq_account | iam | -| common_ | common_product_item, common_event, common_city | catalog / common | +| 前缀 | 示例表 | 负责人 | +|------|--------|--------| +| user_ | user_user, user_order, user_benefit_coupon | jacy-dukang | +| store_ | store_store, store_account, store_payout | 刘京尧 / jacy-dukang(settlement) | +| partner_ | partner_partner, partner_bill | 刘京尧 / jacy-dukang(settlement) | +| hq_ | hq_account | jacy-dukang | +| common_ | common_product_item, common_event, common_city | jacy-dukang | | log_ | log_third_party, log_user_analytics | 写入方 Module | **无以下 V1 表**:payments、user_order_item、redeem_tokens(DB)。 +## Controller 路由(当前实现) + +### iam + +| 路径 | Controller | +|------|-----------| +| `POST /auth/session/bootstrap` | UserAuthController | +| `POST /auth/sms/send` | UserAuthController | +| `POST /auth/sms/login` | UserAuthController | +| `GET /user/me` | UserAuthController | +| `GET/POST/PUT/DELETE /user/addresses` | UserAddressController | +| `POST /shop/auth/*` | ShopAuthController | +| `POST /partner/auth/*` | PartnerAuthController | +| `POST /admin/auth/login` | AdminAuthController | + +### catalog / trade / benefit / redeem + +| 路径 | Module | +|------|--------| +| `GET /catalog/*` | catalog | +| `POST/GET /trade/orders` | trade | +| `GET/POST /benefit/*` | benefit | +| `POST /redeem/*` | redeem(C 端) | +| `POST /shop/redeem/*` | redeem(门店) | + +### store / settlement + +| 路径 | Module | +|------|--------| +| `GET /stores` | store(C 端门店列表) | +| `GET/POST /partner/stores` | store | +| `GET /partner/dashboard` | store | +| `GET /shop/store` | store | +| `GET /shop/dashboard` | store | +| `GET /partner/settlement/*` | settlement | +| `GET /partner/me` | settlement | +| `GET /partner/orders` | trade | + +### ops(admin-web) + +| 路径 | 说明 | +|------|------| +| `/admin/dashboard` | 看板 | +| `/admin/users` | C 端用户 | +| `/admin/orders` | 订单 | +| `/admin/stores` | 门店 | +| `/admin/store-accounts` | 门店账号 | +| `/admin/store-media` | 门店媒体 | +| `/admin/partners` | 合伙人 | +| `/admin/partner-accounts` | 合伙人账号 | +| `/admin/cities` | 开城 | +| `/admin/hq-accounts` | 总部账号 | +| `/admin/benefit/coupons` | 权益券 | +| `/admin/benefit/ledgers` | 权益流水 | +| `/admin/redeem-records` | 核销记录 | +| `/admin/deliveries` | 配送 | + +### analytics / health + +| 路径 | Module | +|------|--------| +| `POST /analytics/*` | analytics | +| `GET /health` | health | + ## 关键跨模块调用链 **Mock/真实支付成功** diff --git a/.cursor/skills/dukang-coding/reference-frontend.md b/.cursor/skills/dukang-coding/reference-frontend.md index c6b3345..4195ee8 100644 --- a/.cursor/skills/dukang-coding/reference-frontend.md +++ b/.cursor/skills/dukang-coding/reference-frontend.md @@ -2,14 +2,18 @@ > 路由事实源:[`pages/ROUTE_MAP.md`](../../../pages/ROUTE_MAP.md) -## App 配置 +## 四 App 概览 -| App | Filter | Dev | X-Client-App | -|-----|--------|-----|--------------| -| h5-user | @dukang/h5-user | pnpm dev:user | USER_H5 | -| h5-shop | @dukang/h5-shop | pnpm dev:shop | SHOP_H5 | -| h5-partner | @dukang/h5-partner | pnpm dev:partner | PARTNER_H5 | -| admin-web | @dukang/admin-web | pnpm dev:admin | AdminAuth(内部) | +| App | 目录 | 端口 | 负责人 | X-Client-App | 原型 | +|-----|------|------|--------|--------------|------| +| h5-user | apps/h5-user | 5173 | jacy-dukang | USER_H5 | pages/user/ | +| h5-shop | apps/h5-shop | 5174 | 刘京尧 | SHOP_H5 | pages/shop/ | +| h5-partner | apps/h5-partner | 5175 | 刘京尧 | PARTNER_H5 | pages/partner/ | +| admin-web | apps/admin-web | 5175 | jacy-dukang | HQ_WEB | pages/hq/ | + +> **h5-partner 与 admin-web 端口同为 5175**,勿同时 `dev:partner` + `dev:admin`。 + +Vite 代理:`/api` → `localhost:3000`。 ## 共享包 @@ -18,42 +22,100 @@ import { OrderStatus, ClientApp } from '@dukang/shared-types'; import { PageHeader } from '@dukang/shared-ui'; ``` -## h5-user 主路由(OWNER A) +H5 三端引用 `@dukang/shared-ui`(`tokens.css`)。admin-web 使用 Ant Design 5,不引用 shared-ui。 -| Route | 页面 | 关键 API | -|-------|------|----------| -| `/login` | 登录 | POST /auth/sms/send, /auth/login/sms | -| `/` | 首页 | GET /catalog/products | -| `/product/:id` | 详情 | GET /catalog/products/:id | -| `/order/confirm` | 确认订单 | POST /trade/orders/preview, /trade/orders | -| `/pay` | Mock 支付 | POST /trade/orders/:id/pay | -| `/orders` | 5 Taber | GET /trade/orders?tab= | -| `/benefit` | 权益 | GET /benefit/coupons | -| `/redeem` | 出码 | POST /redeem/token | -| `/stores` | 门店 | GET /stores(仅 OPEN) | +## h5-user 路由(jacy-dukang) -## h5-shop 主路由(OWNER D) +| 路由 | Page | 主要 API | +|------|------|----------| +| /login | LoginPage | /auth/* | +| / | HomePage | /catalog | +| /product/:id | ProductDetailPage | /catalog | +| /order/confirm | OrderConfirmPage | /trade/orders/preview | +| /pay | PayPage | /trade/orders/:id/pay | +| /orders | OrderListPage | /trade/orders?tab= | +| /orders/:id | OrderDetailPage | /trade/orders/:id | +| /stores | StoreListPage | /stores | +| /stores/:id | StoreDetailPage | /stores/:id | +| /benefit | BenefitPage | /benefit | +| /benefit/:id | BenefitDetailPage | /benefit/:id | +| /redeem | RedeemPage | /redeem | +| /redeem/code | RedeemCodePage | /redeem | +| /redeem/success | RedeemSuccessPage | — | +| /addresses | AddressListPage | /user/addresses | +| /mine | MinePage | /user/me | -| Route | 页面 | 关键 API | -|-------|------|----------| -| `/login` | 登录 | POST /shop/auth/login/sms | -| `/redeem` | 核销确认 | POST /shop/redeem/confirm | -| `/records` | 核销记录 | GET /shop/redeem/records | +### 订单 5 Tab -## h5-partner 主路由(OWNER B) +``` +all | pending_pay | pending_ship | pending_receive | completed +``` -| Route | 页面 | 关键 API | -|-------|------|----------| -| `/stores/new` | 录店 | POST /partner/stores | -| `/stores` | 门店管理 | GET /partner/stores | -| `/orders` | 辖区订单 | GET /partner/orders | +## h5-shop 路由(刘京尧) + +| 路由 | Page | 主要 API | +|------|------|----------| +| /login | LoginPage | /shop/auth/* | +| / | HomePage | /shop/dashboard | +| /redeem | RedeemConfirmPage | /shop/redeem/* | +| /redeem/success | RedeemSuccessPage | — | +| /records | RecordsPage | /shop/redeem/records | +| /status | StatusPage | /shop/store | +| /mine | MinePage | /shop/store | + +## h5-partner 路由(刘京尧) + +| 路由 | Page | 主要 API | +|------|------|----------| +| /login | LoginPage | /partner/auth/* | +| / | HomePage | /partner/dashboard | +| /stores | StoreListPage | /partner/stores | +| /stores/new | StoreCreatePage | /partner/stores | +| /stores/:id | StoreDetailPage | /partner/stores/:id | +| /orders | OrderListPage | /partner/orders | +| /orders/:id | OrderDetailPage | /partner/orders/:id | +| /center | CenterPage | /partner/me | +| /center/bills | BillsPage | /partner/settlement/* | +| /reshipments | ReshipPage | /partner/after-sales/reshipments | +| /reports/weekly | WeeklyReportPage | /partner/reports/weekly | +| /leaderboard | LeaderboardPage | /partner/dashboard/leaderboard | | — | Mock 推进配送 | POST /partner/orders/:id/mock-advance-delivery | +## admin-web 路由(jacy-dukang) + +| 路由 | Page | 主要 API | +|------|------|----------| +| /login | LoginPage | /admin/auth/login | +| / | DashboardPage | /admin/dashboard | +| /users | UsersPage | /admin/users | +| /orders | OrdersPage | /admin/orders | +| /stores | StoresPage | /admin/stores | +| /store-accounts | StoreAccountsPage | /admin/store-accounts | +| /store-media | StoreMediaPage | /admin/store-media | +| /partners | PartnersPage | /admin/partners | +| /partner-accounts | PartnerAccountsPage | /admin/partner-accounts | +| /cities | CitiesPage | /admin/cities | +| /hq-accounts | HqAccountsPage | /admin/hq-accounts | +| /benefit/coupons | BenefitCouponsPage | /admin/benefit/coupons | +| /benefit/ledgers | BenefitLedgersPage | /admin/benefit/ledgers | +| /redeem-records | RedeemRecordsPage | /admin/redeem-records | +| /deliveries | DeliveriesPage | /admin/deliveries | + +## 端 → API 前缀 + +| 端 | API 前缀 | NestJS Module | +|----|----------|---------------| +| C 端 | /auth, /user, /catalog, /trade, /benefit, /redeem, /stores | iam, catalog, trade, benefit, redeem, store | +| 门店 | /shop/auth, /shop/redeem, /shop/store | iam, redeem, store | +| 合伙人 | /partner/* | iam, store, trade, settlement | +| 总部 | /admin/* | iam, catalog, store, trade, settlement, ops | + ## UI 硬约束 - C 端订单 **5 Tab**(含 pending_ship) - 个人中心无会员等级(V1 不做会员体系) - 清香型 4 SKU 可购;酱香/浓香灰态 +- 门店列表仅 `OPEN` 状态 - 原型只读:`pages/{端}/*/code.html`,勿改原型目录 ## preV1 与 V2 UI 差异 diff --git a/.cursor/skills/dukang-project/SKILL.md b/.cursor/skills/dukang-project/SKILL.md new file mode 100644 index 0000000..a6fd205 --- /dev/null +++ b/.cursor/skills/dukang-project/SKILL.md @@ -0,0 +1,68 @@ +--- +name: dukang-project +description: >- + Manages 杜康好客 PRD, requirements review, prototype mapping, and task card + planning. Use when writing PRD, reviewing requirements, mapping prototypes to + features, or splitting task cards. Not for daily coding (use @dukang-coding). +disable-model-invocation: true +--- + +# 杜康好客 · 项目/需求 Skill + +## 何时启用 + +- 写或改 PRD、业务规则 +- 评审需求、对照原型 +- 拆分或规划任务卡 +- 用户 @dukang-project + +**不用于**:日常编码、修 Bug(用 @dukang-coding) + +## 文档阅读顺序 + +| 优先级 | 文档 | 用途 | +|--------|------|------| +| 1 | `杜康好客-V2编码手册.md` §二 | **做什么**(业务规则、状态、边界) | +| 2 | `conventions.md` | **怎么协作**(模块边界、提交) | +| 3 | V2 手册 §四 | **怎么架构**(NestJS 模块) | +| 4 | V2 手册 §五 | **表结构**(Prisma / SQL) | +| 5 | V2 手册 §六 | **接口契约**(四端 JWT) | +| 6 | `pages/{user,shop,partner,hq}/` | **UI 参照** | +| 7 | `杜康好客-preV1编码手册.md` | preV1 联调范围 | + +## 需求变更规则 + +- **只改** `杜康好客-V2编码手册.md` 作为业务事实源 +- 编码 Agent **不得**边写代码边改业务规则 +- 改 API/表结构 → 同步手册 §五/§六 + `packages/shared-types` + +## 原型 → 模块映射 + +| 端 | 原型目录 | 主要 API 前缀 | NestJS Module | +|----|----------|---------------|---------------| +| C 端 | pages/user/ | /auth, /user, /catalog, /trade, /benefit, /redeem, /stores | iam, catalog, trade, benefit, redeem, store | +| 门店 | pages/shop/ | /shop/auth, /shop/redeem, /shop/store | iam, redeem, store | +| 合伙人 | pages/partner/ | /partner/* | iam, store, trade, settlement | +| 总部 | pages/hq/ | /admin/* | iam, catalog, store, trade, settlement, ops | + +## 任务卡格式 + +- ID 格式:`Mx-LAYER-NNN`(如 `M2-BE-TRD-002`) +- 任务卡全集:V2 手册 §七 附录 A +- preV1 任务卡:preV1 手册 §9 + +## 团队 OWNER + +| OWNER | 前端 | 后端 Module | +|-------|------|-------------| +| A | h5-user | iam, trade, benefit, analytics | +| B | h5-partner | store | +| C | admin-web | catalog, settlement, ops | +| D | h5-shop | redeem | +| Lead | packages/* | callbacks, jobs, common | + +## 输出要求 + +- 需求文档变更需标注影响模块与 Owner +- 新任务卡需含:PRD §、API 路径、DB 表、原型路径、验收标准 +- 跨模块 PR 需相关双 Owner Review diff --git a/AGENTS.md b/AGENTS.md index 0de4be8..deddf75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,8 @@ pnpm lint && pnpm test Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 +> **端口冲突**:`h5-partner` 与 `admin-web` 同为 5175,勿同时 `dev:partner` + `dev:admin`。 + ## 文档优先级(冲突时) 1. `杜康好客-V2编码手册.md` §二 — 业务规则 @@ -70,6 +72,16 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 - 业务纯规则 → `packages/domain`;枚举/DTO → `packages/shared-types` - 微信/配送回调入口 **仅** `callbacks/`;Mock 实现 **仅** `integrations/*` +## Cursor 规范 + +| 类型 | 路径 | 说明 | +|------|------|------| +| Rules | `.cursor/rules/` | 自动加载的编码规范 | +| 编码 Skill | `.cursor/skills/dukang-coding/` | @dukang-coding | +| 需求 Skill | `.cursor/skills/dukang-project/` | @dukang-project | +| preV1 Skill | `.cursor/skills/dukang-prev1/` | Mock / Feature Flag | +| 任务卡 Skill | `.cursor/skills/dukang-task-card/` | P1-* / M*-* | + ## 子 Agent(按职责选用) 在 Cursor Agent 输入 `/` 选择: @@ -88,9 +100,17 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。 | Skill | 何时用 | |-------|--------| | `dukang-coding` | 实现功能、修 Bug(通用编码流程) | +| `dukang-project` | PRD/需求评审、原型对照、任务卡规划(**不用于日常编码**) | | `dukang-prev1` | Mock 开关、preV1 裁剪、Feature Flag | | `dukang-task-card` | 领取任务卡 `P1-*` / `M*-*` 并按 DoD 交付 | +## 快速指令 + +- 实现任务卡:`@dukang-coding 实现 P1-M2-002` +- 需求评审:`@dukang-project 对照 pages/partner/ 评审 PRD` +- PR 前审查:`/boundary-reviewer` +- 原型参照:`pages/{user,shop,partner}/`(只读) + ## 核心业务常量(不可偏离) ``` @@ -104,7 +124,7 @@ C 端门店仅 status=OPEN ## 提交与 PR - Conventional Commits:`feat(trade):`、`fix(redeem):`;scope = 端或模块 -- 跨模块 PR → 相关双 OWNER Review +- 跨模块 PR → jacy-dukang + 刘京尧 共同 Review(若涉及双方模块) - 改 API/表 → 同步 V2 手册 §五/§六 + `shared-types` - 不提交 `.env`、`dist/`、`node_modules/` diff --git a/apps/h5-partner/src/App.tsx b/apps/h5-partner/src/App.tsx index 37ef4df..a549bad 100644 --- a/apps/h5-partner/src/App.tsx +++ b/apps/h5-partner/src/App.tsx @@ -9,6 +9,9 @@ import OrderListPage from './pages/OrderListPage'; import OrderDetailPage from './pages/OrderDetailPage'; import CenterPage from './pages/CenterPage'; import BillsPage from './pages/BillsPage'; +import ReshipPage from './pages/ReshipPage'; +import WeeklyReportPage from './pages/WeeklyReportPage'; +import LeaderboardPage from './pages/LeaderboardPage'; export default function App() { return ( @@ -22,6 +25,9 @@ export default function App() { } /> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/apps/h5-partner/src/lib/leaderboard.ts b/apps/h5-partner/src/lib/leaderboard.ts new file mode 100644 index 0000000..bf9f7c9 --- /dev/null +++ b/apps/h5-partner/src/lib/leaderboard.ts @@ -0,0 +1,15 @@ +export type LeaderboardEntry = { + rank: number; + name: string; + role: string; + totalStores: number; + monthStores: number; +}; + +/** preV1:贡献榜 Mock,待 `/partner/dashboard/leaderboard` 接入后替换 */ +export const LEADERBOARD_MOCK: LeaderboardEntry[] = [ + { rank: 1, name: '陈经理', role: '内部员工', totalStores: 45, monthStores: 8 }, + { rank: 2, name: '李华', role: '城市合伙人', totalStores: 32, monthStores: 5 }, + { rank: 3, name: '王拓', role: '推广员', totalStores: 18, monthStores: 3 }, + { rank: 4, name: '赵敏', role: '内部员工', totalStores: 12, monthStores: 2 }, +]; diff --git a/apps/h5-partner/src/lib/storeDraft.ts b/apps/h5-partner/src/lib/storeDraft.ts new file mode 100644 index 0000000..90bb5ed --- /dev/null +++ b/apps/h5-partner/src/lib/storeDraft.ts @@ -0,0 +1,91 @@ +export type StoreDraftForm = { + name: string; + phone: string; + district: string; + address: string; + intro: string; + bankAccountName: string; + bankAccountNo: string; + bankBranch: string; +}; + +export type StoreDraft = { + step: number; + form: StoreDraftForm; +}; + +export const STORE_DRAFT_KEY = 'partner_store_draft_v1'; + +export const defaultStoreForm = (): StoreDraftForm => ({ + name: '', + phone: '', + district: '金水区', + address: '', + intro: '', + bankAccountName: '', + bankAccountNo: '', + bankBranch: '', +}); + +function normalizeForm(raw: Record): StoreDraftForm { + const base = defaultStoreForm(); + return { + name: String(raw.name ?? base.name), + phone: String(raw.phone ?? base.phone), + district: String(raw.district ?? base.district), + address: String(raw.address ?? base.address), + intro: String(raw.intro ?? base.intro), + bankAccountName: String(raw.bankAccountName ?? base.bankAccountName), + bankAccountNo: String(raw.bankAccountNo ?? base.bankAccountNo), + bankBranch: String(raw.bankBranch ?? base.bankBranch), + }; +} + +export function loadStoreDraft(): StoreDraft | null { + try { + const raw = localStorage.getItem(STORE_DRAFT_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Record; + if (parsed.form && typeof parsed.form === 'object') { + return { + step: Math.min(3, Math.max(1, Number(parsed.step) || 1)), + form: normalizeForm(parsed.form as Record), + }; + } + return { step: 1, form: normalizeForm(parsed) }; + } catch { + return null; + } +} + +export function saveStoreDraft(draft: StoreDraft) { + localStorage.setItem(STORE_DRAFT_KEY, JSON.stringify(draft)); +} + +export function clearStoreDraft() { + localStorage.removeItem(STORE_DRAFT_KEY); +} + +const PHONE_RE = /^1\d{10}$/; +const BANK_RE = /^\d{16,19}$/; + +export function validateStoreStep1(form: StoreDraftForm): string | null { + if (!form.name.trim()) return '请填写门店名称'; + if (!form.phone.trim()) return '请填写联系电话'; + if (!PHONE_RE.test(form.phone.trim())) return '联系电话须为11位手机号'; + if (!form.district.trim()) return '请填写区县'; + if (!form.address.trim()) return '请填写详细地址'; + if (form.intro.trim()) { + const len = form.intro.trim().length; + if (len < 10 || len > 500) return '门店简介须为 10~500 字'; + } + return null; +} + +export function validateStoreStep3(form: StoreDraftForm): string | null { + if (!form.bankAccountName.trim()) return '请填写户主姓名'; + if (!form.bankAccountNo.trim()) return '请填写银行卡号'; + if (!BANK_RE.test(form.bankAccountNo.replace(/\s/g, ''))) return '银行卡号须为 16~19 位数字'; + if (!form.bankBranch.trim()) return '请填写开户支行'; + return null; +} diff --git a/apps/h5-partner/src/pages/CenterPage.tsx b/apps/h5-partner/src/pages/CenterPage.tsx index 06d43c7..a9b726f 100644 --- a/apps/h5-partner/src/pages/CenterPage.tsx +++ b/apps/h5-partner/src/pages/CenterPage.tsx @@ -103,13 +103,34 @@ export default function CenterPage() {
description
- 待确认账单 + 财务对账
{pendingBills.length > 0 && {pendingBills.length}} chevron_right
+ +
+
monitoring
+ 数据周报 +
+ chevron_right + + +
+
assignment_return
+ 补发处理 +
+ chevron_right + + +
+
military_tech
+ 合伙人贡献榜 +
+ chevron_right + diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 367680f..00ea2ed 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -1,29 +1,60 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { isLoggedIn, request } from '../lib/api'; +import { LEADERBOARD_MOCK } from '../lib/leaderboard'; function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } +function auditStatus(store: Record) { + const audits = store.audits as Array<{ status?: string }> | undefined; + return String(audits?.[0]?.status ?? '').toUpperCase(); +} + +function storeStatusLabel(store: Record) { + const audit = auditStatus(store); + if (audit === 'PENDING') return '待审核'; + const s = String(store.status).toUpperCase(); + if (s === 'OPEN') return '营业中'; + if (s === 'PAUSED') return '暂时闭店'; + if (s === 'CLOSED') return '已关闭'; + return s; +} + export default function HomePage() { const navigate = useNavigate(); const [dash, setDash] = useState | null>(null); + const [stores, setStores] = useState>>([]); useEffect(() => { if (!isLoggedIn()) { navigate('/login'); return; } - request('PARTNER_H5', '/partner/dashboard').then(setDash); + request>('PARTNER_H5', '/partner/dashboard').then(setDash); + request>>('PARTNER_H5', '/partner/stores').then(setStores); }, [navigate]); - const storeCount = Number(dash?.storeCount || 0); + const storeCount = Number(dash?.storeCount || stores.length || 0); const orderCount = Number(dash?.orderCount || 0); - const activeStores = Math.max(0, storeCount - Math.ceil(storeCount * 0.05)); - const abnormalStores = storeCount - activeStores; + const activeStores = stores.filter((s) => String(s.status).toUpperCase() === 'OPEN').length; + const abnormalStores = Math.max(0, storeCount - activeStores); + const pendingStores = useMemo( + () => stores.filter((s) => auditStatus(s) === 'PENDING'), + [stores], + ); const revenue = orderCount * 128.45; const profit = revenue * 0.25; const pendingShip = Math.ceil(orderCount * 0.04); const shipping = Math.ceil(orderCount * 0.12); const completed = Math.max(0, orderCount - pendingShip - shipping); + const monthNew = stores.filter((s) => { + const created = new Date(String(s.createdAt || '')); + const now = new Date(); + return created.getMonth() === now.getMonth() && created.getFullYear() === now.getFullYear(); + }).length; + + const recentFromDash = (dash?.recentStores as Array> | undefined) ?? []; + const displayStores = stores.length > 0 ? stores : recentFromDash; + const previewLeaderboard = LEADERBOARD_MOCK.slice(0, 2); return (
@@ -87,7 +118,7 @@ export default function HomePage() {
录入新店 - +
assignment_return {pendingShip > 0 && {pendingShip}} @@ -100,7 +131,7 @@ export default function HomePage() {
财务对账 - +
monitoring
@@ -139,17 +170,69 @@ export default function HomePage() {

本月新增签约

- {Math.ceil(storeCount * 0.08)} - - trending_up 18% - + {monthNew}

待审核门店

-

{Math.ceil(storeCount * 0.03)}

+

{pendingStores.length}

+ + {pendingStores.length > 0 && ( +
+

待审核门店名称

+
    + {pendingStores.map((s) => ( +
  • + {String(s.name || '未命名门店')} +
  • + ))} +
+
+ )} + + {displayStores.length > 0 && ( +
    +
  • + 门店名称 + 状态 +
  • + {displayStores.slice(0, 5).map((s) => ( +
  • + {String(s.name || '未命名门店')} + {storeStatusLabel(s)} +
  • + ))} +
+ )} + +
+
+

+ military_tech + 合伙人贡献榜 +

+ 查看全部 +
+ {previewLeaderboard.map((entry) => ( +
+
{entry.rank}
+
+

+ {entry.name} + ({entry.role}) +

+

累计拓店 {entry.totalStores} 间

+
+
+

{entry.monthStores} 间

+

本月新增

+
+
+ ))} +
+

{String(dash?.companyName || '郑州合伙人')} · 辖区管理

diff --git a/apps/h5-partner/src/pages/LeaderboardPage.tsx b/apps/h5-partner/src/pages/LeaderboardPage.tsx new file mode 100644 index 0000000..e2f2885 --- /dev/null +++ b/apps/h5-partner/src/pages/LeaderboardPage.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from 'react-router-dom'; +import PageHeader from '@dukang/shared-ui/PageHeader'; +import { LEADERBOARD_MOCK } from '../lib/leaderboard'; + +export default function LeaderboardPage() { + const navigate = useNavigate(); + + return ( +
+ navigate('/')} /> + +

+ 按拓店数排行,数据每周更新 +

+ +
+ {LEADERBOARD_MOCK.map((entry) => ( +
+
{entry.rank}
+
+

+ {entry.name} + ({entry.role}) +

+

累计拓店 {entry.totalStores} 间

+
+
+

{entry.monthStores} 间

+

本月新增

+
+
+ ))} +
+
+ ); +} diff --git a/apps/h5-partner/src/pages/ReshipPage.tsx b/apps/h5-partner/src/pages/ReshipPage.tsx new file mode 100644 index 0000000..d9c86af --- /dev/null +++ b/apps/h5-partner/src/pages/ReshipPage.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import PageHeader from '@dukang/shared-ui/PageHeader'; + +type ReshipStatus = 'ALL' | 'PENDING' | 'SHIPPING' | 'DONE'; + +const MOCK_ITEMS = [ + { id: '1', orderNo: 'DK20260301001', status: 'PENDING' as const, productName: '杜康窖藏', originalOrderNo: 'DK20260228088', address: '郑州市金水区花园路 88 号' }, + { id: '2', orderNo: 'DK20260302002', status: 'SHIPPING' as const, productName: '杜康特曲', originalOrderNo: 'DK20260215012', address: '郑州市二七区大学路 12 号' }, +]; + +const STATUS_LABEL: Record = { + PENDING: '待配送', + SHIPPING: '配送中', + DONE: '已完成', +}; + +const FILTERS: { key: ReshipStatus; label: string }[] = [ + { key: 'ALL', label: '全部' }, + { key: 'PENDING', label: '待配送' }, + { key: 'SHIPPING', label: '配送中' }, + { key: 'DONE', label: '已完成' }, +]; + +/** BUWR-23:补发处理 */ +export default function ReshipPage() { + const navigate = useNavigate(); + const [filter, setFilter] = useState('ALL'); + + const list = MOCK_ITEMS.filter((item) => filter === 'ALL' || item.status === filter); + + return ( +
+ navigate('/')} /> + +

+ 总部下达的补发工单,确认后可推进配送 +

+ +
+ {FILTERS.map((f) => ( + + ))} +
+ + {list.length === 0 &&
暂无补发工单
} + + {list.map((item) => ( +
+
+ 原单 {item.originalOrderNo} + {STATUS_LABEL[item.status]} +
+

{item.productName}

+

补发单号 {item.orderNo}

+
+ location_on +

{item.address}

+
+ {item.status === 'PENDING' && ( + + )} +
+ ))} +
+ ); +} diff --git a/apps/h5-partner/src/pages/StoreCreatePage.tsx b/apps/h5-partner/src/pages/StoreCreatePage.tsx index e6c9ec7..9aeb050 100644 --- a/apps/h5-partner/src/pages/StoreCreatePage.tsx +++ b/apps/h5-partner/src/pages/StoreCreatePage.tsx @@ -1,36 +1,77 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import PageHeader from '@dukang/shared-ui/PageHeader'; import { request } from '../lib/api'; +import { + clearStoreDraft, + defaultStoreForm, + loadStoreDraft, + saveStoreDraft, + type StoreDraftForm, + validateStoreStep1, + validateStoreStep3, +} from '../lib/storeDraft'; const STEPS = ['基本信息', '照片上传', '结算资质'] as const; export default function StoreCreatePage() { const navigate = useNavigate(); const [params, setParams] = useSearchParams(); - const step = Number(params.get('step') || 1); - const [form, setForm] = useState({ - name: '', - phone: '', - district: '金水区', - address: '', - intro: '', - accountPhone: '', - accountName: '', - bankAccountName: '', - bankAccountNo: '', - bankBranch: '', - }); + const saved = loadStoreDraft(); + const [form, setForm] = useState(saved?.form ?? defaultStoreForm()); + const [error, setError] = useState(''); + + const stepFromUrl = Number(params.get('step') || 0); + const step = stepFromUrl >= 1 && stepFromUrl <= 3 ? stepFromUrl : (saved?.step ?? 1); + + useEffect(() => { + if (!stepFromUrl && saved?.step) { + setParams({ step: String(saved.step) }, { replace: true }); + } + }, [stepFromUrl, saved?.step, setParams]); + + useEffect(() => { + saveStoreDraft({ step, form }); + }, [step, form]); + + function patchForm(patch: Partial) { + setForm((prev) => ({ ...prev, ...patch })); + setError(''); + } function goStep(n: number) { setParams({ step: String(n) }); + setError(''); + } + + function handleNext() { + if (step === 1) { + const msg = validateStoreStep1(form); + if (msg) { setError(msg); return; } + } + goStep(step + 1); } async function submit() { + const msg = validateStoreStep3(form); + if (msg) { setError(msg); return; } + const step1Msg = validateStoreStep1(form); + if (step1Msg) { setError(step1Msg); return; } + await request('PARTNER_H5', '/partner/stores', { method: 'POST', - body: JSON.stringify(form), + body: JSON.stringify({ + name: form.name.trim(), + phone: form.phone.trim(), + district: form.district.trim(), + address: form.address.trim(), + intro: form.intro.trim() || undefined, + bankAccountName: form.bankAccountName.trim(), + bankAccountNo: form.bankAccountNo.replace(/\s/g, ''), + bankBranch: form.bankBranch.trim(), + }), }); + clearStoreDraft(); navigate('/stores'); } @@ -61,6 +102,10 @@ export default function StoreCreatePage() { + {error && ( +

{error}

+ )} + {step === 1 && ( <>
@@ -72,30 +117,30 @@ export default function StoreCreatePage() {
store - setForm({ ...form, name: e.target.value })} /> + patchForm({ name: e.target.value })} />
call - setForm({ ...form, phone: e.target.value })} /> + patchForm({ phone: e.target.value })} />
location_on - setForm({ ...form, district: e.target.value })} /> + patchForm({ district: e.target.value })} />
-