v3计划内容上传
This commit is contained in:
@@ -50,7 +50,7 @@ Never scatter `if (process.env.MOCK_PAY)` in trade/benefit services.
|
||||
1. Align with V2 manual §五
|
||||
2. Notify table owner: jacy-dukang 或 刘京尧(store/redeem 相关表)
|
||||
3. `pnpm db:validate` + seed still works
|
||||
4. PR: jacy-dukang Review;涉及 store/redeem 表时 @刘京尧
|
||||
4. PR: jacy-dukang Review;涉及 store/redeem 表时通知刘京尧
|
||||
|
||||
## Forbidden
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ Inspect the diff (or named files) for:
|
||||
|
||||
| 逻辑域 | Git 账号 | Apps | Modules |
|
||||
|--------|----------|------|---------|
|
||||
| 主责 | jacy-dukang | h5-user, admin-web | iam, trade, benefit, analytics, catalog, settlement, ops |
|
||||
| 合伙人+门店 | 刘京尧 | h5-partner, h5-shop | store, redeem |
|
||||
| 主责 | jacy-dukang | h5-user, admin-web, 门店 | iam, trade, benefit, analytics, catalog, settlement, ops |
|
||||
| 合伙人 | 刘京尧 | h5-partner, h5-shop | store, redeem |
|
||||
| 横切 | jacy-dukang | — | packages, callbacks, jobs, common, integrations |
|
||||
|
||||
逻辑 A/B/C/D 边界仍有效;刘京尧 同时负责 B+D,但 **store 与 redeem 模块仍不可互写表**。
|
||||
|
||||
@@ -36,7 +36,7 @@ User (Owner A) generates token → Shop scans/confirms (you)
|
||||
## Hard rules
|
||||
|
||||
- Redis token: `redeem:token:{token}` TTL 300s
|
||||
- Amount: `0 < amount ≤ min(balance, 500)`
|
||||
- Amount: direct redeem `0 < amount ≤ total ACTIVE benefit balance`; document redeem `0 < amount ≤ document balance`
|
||||
- Transaction + coupon `version` optimistic lock
|
||||
- **Never** `prisma.order.update` in redeem module
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ alwaysApply: true
|
||||
## 核心业务常量
|
||||
|
||||
- 权益额 = `benefit_amount ?? price`
|
||||
- 核销:0 < amount ≤ min(balance, **500**)
|
||||
- 核销:直接核销 `0 < amount ≤ 全部 ACTIVE 权益总余额`;带单据核销 `0 < amount ≤ 该单据可用金额`
|
||||
- 订单 Tab:`all | pending_pay | pending_ship | pending_receive | completed`
|
||||
- API:`/api/v1`,响应 `{ code, message, data }`
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ src/
|
||||
| App | 端口 | 负责人 | 样式 | X-Client-App |
|
||||
|-----|------|--------|------|--------------|
|
||||
| h5-user | 5173 | jacy-dukang | shared-ui tokens | USER_H5 |
|
||||
| h5-shop | 5174 | 刘京尧 | shared-ui tokens | SHOP_H5 |
|
||||
| h5-shop | 5174 | jacy-dukang | shared-ui tokens | SHOP_H5 |
|
||||
| h5-partner | 5175 | 刘京尧 | shared-ui tokens | PARTNER_H5 |
|
||||
| admin-web | 5175 | jacy-dukang | Ant Design 5 | HQ_WEB |
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ alwaysApply: false
|
||||
## domain
|
||||
|
||||
- 纯函数,无 IO(无 Prisma/Redis/HTTP)
|
||||
- 起购 2/6 瓶、权益 `benefitAmount ?? price`、核销 ¥500 上限在此实现
|
||||
- 起购 2/6 瓶、权益 `benefitAmount ?? price`、V3 两路径核销规则在此实现
|
||||
- 变更必须有单元测试
|
||||
|
||||
```typescript
|
||||
const benefitAmount = product.benefitAmount ?? product.price;
|
||||
// 同城起购 2 瓶 / 跨城 6 瓶
|
||||
// 核销上限 ¥500
|
||||
// 直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额
|
||||
```
|
||||
|
||||
## shared-ui
|
||||
|
||||
@@ -81,14 +81,14 @@ await this.prisma.order.update({ ... });
|
||||
## 数据库变更
|
||||
|
||||
1. 改 `server/dukang-api/prisma/schema.prisma` 对齐手册 §五
|
||||
2. 迁移需 OWNER Review(store/redeem 表 → @刘京尧)
|
||||
2. 迁移需 OWNER Review(store/redeem 表 → 刘景尧)
|
||||
3. 初始化 SQL:`server/dukang-api/prisma/init_v3.sql`
|
||||
|
||||
## 核心业务(packages/domain)
|
||||
|
||||
```typescript
|
||||
const benefitAmount = product.benefitAmount ?? product.price;
|
||||
// 核销:0 < amount ≤ min(balance, 500)
|
||||
// 核销:直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额
|
||||
// 同城 min 2 瓶 / 跨城 min 6 瓶
|
||||
|
||||
// 支付成功 → log_third_party + user_order.pay_status=PAID
|
||||
|
||||
@@ -36,8 +36,8 @@ Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。
|
||||
| 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` | 刘京尧 |
|
||||
| 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 |
|
||||
@@ -48,8 +48,8 @@ Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。
|
||||
| 前缀 | 示例表 | 负责人 |
|
||||
|------|--------|--------|
|
||||
| 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) |
|
||||
| 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 |
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
| 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/ |
|
||||
| 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`。
|
||||
@@ -51,7 +51,7 @@ H5 三端引用 `@dukang/shared-ui`(`tokens.css`)。admin-web 使用 Ant Des
|
||||
all | pending_pay | pending_ship | pending_receive | completed
|
||||
```
|
||||
|
||||
## h5-shop 路由(刘京尧)
|
||||
## h5-shop 路由(刘景尧)
|
||||
|
||||
| 路由 | Page | 主要 API |
|
||||
|------|------|----------|
|
||||
@@ -63,7 +63,7 @@ all | pending_pay | pending_ship | pending_receive | completed
|
||||
| /status | StatusPage | /shop/store |
|
||||
| /mine | MinePage | /shop/store |
|
||||
|
||||
## h5-partner 路由(刘京尧)
|
||||
## h5-partner 路由(刘景尧)
|
||||
|
||||
| 路由 | Page | 主要 API |
|
||||
|------|------|----------|
|
||||
|
||||
@@ -31,8 +31,8 @@ description: >-
|
||||
|------|--------|---------|
|
||||
| C 端 FE、h5-user、交易后端 | jacy-dukang | P1-M1-002, M2-* |
|
||||
| admin、catalog、settlement | jacy-dukang | M1-BE-CAT-* |
|
||||
| 合伙人、store | 刘京尧 | P1-M4-001 |
|
||||
| 门店核销、h5-shop | 刘京尧 | P1-M3-002 |
|
||||
| 合伙人、store | 刘景尧 | P1-M4-001 |
|
||||
| 门店核销、h5-shop | 刘景尧 | P1-M3-002 |
|
||||
| Monorepo/integrations | jacy-dukang | P1-M0-* |
|
||||
|
||||
## preV1 任务卡(精简)
|
||||
@@ -47,7 +47,7 @@ description: >-
|
||||
| P1-M2-001 | 起购校验 2/6 瓶 |
|
||||
| P1-M2-002 | Mock 支付发券 |
|
||||
| P1-M2-003 | 5 Tab 含 pending_ship |
|
||||
| P1-M3-001 | 核销 ¥500 上限 |
|
||||
| P1-M3-001 | V3 两路径核销金额限制 |
|
||||
| P1-M3-002 | 门店扫码核销 |
|
||||
| P1-M4-001 | 录店 AUTO_APPROVE → C 端可见 |
|
||||
| P1-M5-001 | 配送自动推进到 COMPLETED |
|
||||
|
||||
@@ -54,16 +54,16 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。
|
||||
| 负责人 | Git 账号 | 职责 |
|
||||
|--------|----------|------|
|
||||
| **Jacy**(管理员) | `jacy-dukang` | C 端、admin-web、后端主模块、packages、Prisma 迁移主 Review |
|
||||
| **刘京尧** | `刘京尧` | 合伙人 H5、门店 H5、`store` / `redeem` 模块 |
|
||||
| **刘景尧** | `刘景尧` | 合伙人 H5、门店 H5、`store` / `redeem` 模块 |
|
||||
|
||||
逻辑模块边界仍按 A/B/C/D 划分(便于 Agent 隔离),**人员合并**如下:
|
||||
|
||||
| 逻辑 OWNER | 负责人 | 可改路径 | 后端 Module |
|
||||
|------------|--------|----------|-------------|
|
||||
| **A + C + Lead** | jacy-dukang | `apps/h5-user/`, `apps/admin-web/`, `packages/*`, `callbacks/`, `jobs/`, `common/`, `integrations/` | `iam`, `trade`, `benefit`, `analytics`, `catalog`, `settlement`, `ops` |
|
||||
| **B + D** | 刘京尧 | `apps/h5-partner/`, `apps/h5-shop/` | `store`, `redeem` |
|
||||
| **B + D** | 刘景尧 | `apps/h5-partner/`, `apps/h5-shop/` | `store`, `redeem` |
|
||||
|
||||
**Prisma 迁移**:jacy-dukang 主 Review;若改 `store_*` / 核销相关表,需 `@刘京尧` 共同 Review。
|
||||
**Prisma 迁移**:jacy-dukang 主 Review;若改 `store_*` / 核销相关表,需刘景尧共同 Review。
|
||||
|
||||
### 跨模块规则(R1–R8 摘要)
|
||||
|
||||
@@ -91,8 +91,8 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。
|
||||
| `owner-a-user-trade` | jacy-dukang | C 端 H5、订单、支付、权益、埋点 |
|
||||
| `owner-c-catalog-ops` | jacy-dukang | admin-web、开城商品、结算、运营 |
|
||||
| `backend-lead` | jacy-dukang | packages、callbacks、jobs、Prisma 横切 |
|
||||
| `owner-b-partner-store` | 刘京尧 | 合伙人 H5、门店 CRUD/审核 |
|
||||
| `owner-d-shop-redeem` | 刘京尧 | 门店 H5、核销 |
|
||||
| `owner-b-partner-store` | 刘景尧 | 合伙人 H5、门店 CRUD/审核 |
|
||||
| `owner-d-shop-redeem` | 刘景尧 | 门店 H5、核销 |
|
||||
| `boundary-reviewer` | — | PR 前只读审查跨模块违规 |
|
||||
|
||||
## Skills(按需 @)
|
||||
@@ -116,7 +116,7 @@ Mock 验证码:`123456`。测试账号见 [`README.md`](./README.md)。
|
||||
```
|
||||
权益额 = benefit_amount ?? price
|
||||
同城起购 2 瓶 / 跨城 6 瓶
|
||||
核销:0 < amount ≤ min(balance, 500);Redis 码 5 分钟
|
||||
核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额;Redis 码 5 分钟
|
||||
C 端门店仅 status=OPEN
|
||||
订单 Tab:all | pending_pay | pending_ship | pending_receive | completed
|
||||
```
|
||||
@@ -124,7 +124,7 @@ C 端门店仅 status=OPEN
|
||||
## 提交与 PR
|
||||
|
||||
- Conventional Commits:`feat(trade):`、`fix(redeem):`;scope = 端或模块
|
||||
- 跨模块 PR → jacy-dukang + 刘京尧 共同 Review(若涉及双方模块)
|
||||
- 跨模块 PR → jacy-dukang + 刘景尧 共同 Review(若涉及双方模块)
|
||||
- 改 API/表 → 同步 V2 手册 §五/§六 + `shared-types`
|
||||
- 不提交 `.env`、`dist/`、`node_modules/`
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
| 负责人 | Git 账号 | App(preV1 → V2) | Module |
|
||||
|--------|----------|-------------------|--------|
|
||||
| Jacy | jacy-dukang | h5-user、admin-web → mini-user/mini-hq | iam, trade, benefit, analytics, catalog, settlement, ops |
|
||||
| 刘京尧 | 刘京尧 | h5-partner、h5-shop → mini-partner | store, redeem |
|
||||
| 刘景尧 | 刘景尧 | h5-partner、h5-shop → mini-partner | store, redeem |
|
||||
| Jacy(横切) | jacy-dukang | packages/*, callbacks/, jobs/, integrations/ | 基础设施 |
|
||||
|
||||
**禁止**:Module A 直写 Module B 的 Prisma 表;apps import server 源码。
|
||||
@@ -93,7 +93,7 @@
|
||||
```text
|
||||
权益发放额 = common_product_item.benefit_amount ?? price
|
||||
同城起购 2 瓶 / 跨城 6 瓶
|
||||
核销:0 < amount ≤ min(balance, 500)
|
||||
核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额
|
||||
C 端门店列表仅 status=OPEN
|
||||
订单 Tab:all | pending_pay | pending_ship | pending_receive | completed
|
||||
支付回调幂等 → 发券 → common_event(BENEFIT_LEDGER, GRANT)
|
||||
|
||||
+4
-4
@@ -7,8 +7,8 @@
|
||||
| App | 端口 | X-Client-App | 负责人 | 原型 |
|
||||
|-----|------|--------------|--------|------|
|
||||
| `h5-user` | 5173 | `USER_H5` | jacy-dukang | `pages/user/` |
|
||||
| `h5-shop` | 5174 | `SHOP_H5` | 刘京尧 | `pages/shop/` |
|
||||
| `h5-partner` | 5175 | `PARTNER_H5` | 刘京尧 | `pages/partner/` |
|
||||
| `h5-shop` | 5174 | `SHOP_H5` | 刘景尧 | `pages/shop/` |
|
||||
| `h5-partner` | 5175 | `PARTNER_H5` | 刘景尧 | `pages/partner/` |
|
||||
| `admin-web` | — | (Admin JWT) | jacy-dukang | preV1 内部 HQ 替代 |
|
||||
|
||||
V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店仍 H5)。
|
||||
@@ -36,13 +36,13 @@ V2 目标:`mini-user` / `mini-partner` / `mini-hq` 替换对应 H5(除门店
|
||||
|
||||
**勿改**:门店核销确认 UI(属 h5-shop)、合伙人录店(属 h5-partner)
|
||||
|
||||
### h5-shop(刘京尧)
|
||||
### h5-shop(刘景尧)
|
||||
|
||||
主链路:门店登录 → 首页 → 扫码/输入核销 → 确认 → 记录 → 营业状态
|
||||
|
||||
**勿改**:C 端出码页面(属 h5-user)
|
||||
|
||||
### h5-partner(刘京尧)
|
||||
### h5-partner(刘景尧)
|
||||
|
||||
主链路:登录 → 工作台 → 录店 → 门店列表 → 辖区订单 → Mock 推进配送(preV1)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ function WechatOAuthHandler() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv() || !location.search.includes('code=')) return;
|
||||
if (location.pathname === '/login') return;
|
||||
void handlePartnerWechatCallback()
|
||||
.then((result) => {
|
||||
if (!result || !savePartnerWechatAuth(result)) return;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { request, saveAuth } from '../lib/api';
|
||||
import {
|
||||
authorizePartnerWechat,
|
||||
handlePartnerWechatCallback,
|
||||
savePartnerWechatAuth,
|
||||
} from '../lib/wechat-auth';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -9,7 +15,21 @@ export default function LoginPage() {
|
||||
const [phone, setPhone] = useState('13700000001');
|
||||
const [code, setCode] = useState('123456');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wechatLoading, setWechatLoading] = useState(false);
|
||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv()) return;
|
||||
void handlePartnerWechatCallback()
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
if (savePartnerWechatAuth(result)) {
|
||||
navigate('/');
|
||||
}
|
||||
})
|
||||
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
|
||||
}, [navigate]);
|
||||
|
||||
async function login() {
|
||||
setLoading(true);
|
||||
@@ -29,6 +49,21 @@ export default function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function wechatLogin() {
|
||||
setMsg('');
|
||||
if (!isWechatEnv()) {
|
||||
setMsg('请在微信内打开以使用微信一键登录');
|
||||
return;
|
||||
}
|
||||
setWechatLoading(true);
|
||||
try {
|
||||
await authorizePartnerWechat();
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
||||
setWechatLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function sendCode() {
|
||||
if (codeCooldown > 0) return;
|
||||
request('PARTNER_H5', '/partner/auth/sms/send', {
|
||||
@@ -122,10 +157,17 @@ export default function LoginPage() {
|
||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||
</button>
|
||||
<div className="partner-auth-divider"><span>其他登录方式</span></div>
|
||||
<button type="button" className="partner-btn-outline" style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
|
||||
disabled={wechatLoading || loading}
|
||||
onClick={() => void wechatLogin()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#07C160"><path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" /></svg>
|
||||
微信一键登录
|
||||
{wechatLoading ? '跳转授权中…' : '微信一键登录'}
|
||||
</button>
|
||||
{msg && <p className="partner-form-error" role="alert">{msg}</p>}
|
||||
<label className="partner-checkbox-row">
|
||||
<input type="checkbox" />
|
||||
<span>
|
||||
|
||||
+7
-7
@@ -12,7 +12,7 @@
|
||||
| Git 账号 | 角色 | 主责 |
|
||||
|----------|------|------|
|
||||
| `jacy-dukang` | 管理员 + 主责开发 | C 端、admin-web、后端主模块、横切 |
|
||||
| `刘京尧` | 开发 | 合伙人 H5、门店 H5、store/redeem 模块 |
|
||||
| `刘景尧` | 刘景尧 | 合伙人 H5、门店 H5、store/redeem 模块 |
|
||||
|
||||
端与后端模块边界:
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
|------|--------|------|
|
||||
| `apps/h5-user` | jacy-dukang | C 端 H5(V2 → mini-user) |
|
||||
| `apps/admin-web` | jacy-dukang | preV1 总部替代 |
|
||||
| `apps/h5-partner` | 刘京尧 | 合伙人 H5 |
|
||||
| `apps/h5-shop` | 刘京尧 | 门店 H5 |
|
||||
| `apps/h5-partner` | 刘景尧 | 合伙人 H5 |
|
||||
| `apps/h5-shop` | 刘景尧 | 门店 H5 |
|
||||
| `server/.../modules/{iam,trade,benefit,analytics}` | jacy-dukang | 交易与权益域 |
|
||||
| `server/.../modules/{catalog,settlement,ops}` | jacy-dukang | 开城与结算域 |
|
||||
| `server/.../modules/store` | 刘京尧 | 门店域 |
|
||||
| `server/.../modules/redeem` | 刘京尧 | 核销域 |
|
||||
| `server/.../modules/store` | 刘景尧 | 门店域 |
|
||||
| `server/.../modules/redeem` | 刘景尧 | 核销域 |
|
||||
| `server/.../callbacks`, `jobs`, `common`, `integrations` | jacy-dukang | 回调与任务 |
|
||||
| `packages/*` | jacy-dukang 主责;破坏性改动 @刘京尧 | 公共契约 |
|
||||
| `server/dukang-api/prisma` | jacy-dukang + 刘京尧(涉及 store/redeem 表时) | 迁移 Review |
|
||||
| `packages/*` | jacy-dukang 主责;破坏性改动通知刘景尧 | 公共契约 |
|
||||
| `server/dukang-api/prisma` | jacy-dukang + 刘景尧(涉及 store/redeem 表时) | 迁移 Review |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,12 +31,25 @@ describe('validateMinPurchase', () => {
|
||||
});
|
||||
|
||||
describe('validateRedeemAmount', () => {
|
||||
it('rejects over balance or non-positive', () => {
|
||||
it('rejects over balance or non-positive amount', () => {
|
||||
expect(validateRedeemAmount(100, 50).ok).toBe(true);
|
||||
expect(validateRedeemAmount(100, 100).ok).toBe(true);
|
||||
expect(validateRedeemAmount(50, 60).ok).toBe(false);
|
||||
expect(validateRedeemAmount(100, 0).ok).toBe(false);
|
||||
});
|
||||
|
||||
it('allows direct redeem up to total balance without a document cap', () => {
|
||||
expect(validateRedeemAmount(700, 650).ok).toBe(true);
|
||||
expect(validateRedeemAmount(700, 701).ok).toBe(false);
|
||||
});
|
||||
|
||||
it('caps document-based redeem by document amount', () => {
|
||||
expect(validateRedeemAmount(700, 300, 300).ok).toBe(true);
|
||||
expect(validateRedeemAmount(700, 301, 300)).toEqual({
|
||||
ok: false,
|
||||
message: '核销金额不能超过该核销单可用金额',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcRedeemSettleAmount', () => {
|
||||
@@ -65,6 +78,13 @@ describe('allocateBenefitCoupons', () => {
|
||||
it('rejects when total balance insufficient', () => {
|
||||
expect(allocateBenefitCoupons(coupons, 800).ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects when a document cap is lower than requested amount', () => {
|
||||
expect(allocateBenefitCoupons(coupons, 301, 300)).toEqual({
|
||||
ok: false,
|
||||
message: '核销金额不能超过该核销单可用金额',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcBenefitSummary', () => {
|
||||
|
||||
@@ -29,9 +29,13 @@ export function validateMinPurchase(
|
||||
export function validateRedeemAmount(
|
||||
balance: number,
|
||||
amount: number,
|
||||
documentAmount?: number | null,
|
||||
): { ok: boolean; message?: string } {
|
||||
if (amount <= 0) return { ok: false, message: '核销金额必须大于 0' };
|
||||
if (amount > balance) return { ok: false, message: '核销金额不能超过可用余额' };
|
||||
if (documentAmount != null && amount > documentAmount) {
|
||||
return { ok: false, message: '核销金额不能超过该核销单可用金额' };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -45,12 +49,13 @@ export interface BenefitCouponBalance {
|
||||
export function allocateBenefitCoupons(
|
||||
coupons: BenefitCouponBalance[],
|
||||
amount: number,
|
||||
documentAmount?: number | null,
|
||||
): { ok: true; allocations: Array<{ couponId: string; amount: number }> } | { ok: false; message: string } {
|
||||
const active = coupons
|
||||
.filter((c) => c.balance > 0)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
const totalBalance = active.reduce((sum, c) => sum + c.balance, 0);
|
||||
const check = validateRedeemAmount(totalBalance, amount);
|
||||
const check = validateRedeemAmount(totalBalance, amount, documentAmount);
|
||||
if (!check.ok) return { ok: false, message: check.message! };
|
||||
|
||||
let remaining = amount;
|
||||
|
||||
@@ -77,5 +77,6 @@ export const CLIENT_APP_ACTOR_MAP: Record<ClientApp, ActorType> = {
|
||||
[ClientApp.SHOP_H5]: ActorType.STORE,
|
||||
};
|
||||
|
||||
export const REDEEM_MAX_AMOUNT = 500;
|
||||
export const REDEEM_DIRECT_LIMIT_POLICY = 'TOTAL_ACTIVE_BALANCE';
|
||||
export const REDEEM_DOCUMENT_LIMIT_POLICY = 'DOCUMENT_BALANCE';
|
||||
export const REDEEM_TOKEN_TTL_SECONDS = 300;
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|--------|-------------------|--------|
|
||||
| **iam** | User, UserAddress, … | jacy-dukang |
|
||||
| **catalog** | CommonCity, CommonProductItem, … | jacy-dukang |
|
||||
| **store** | Store, Partner | 刘京尧 |
|
||||
| **store** | Store, Partner | 刘景尧 |
|
||||
| **trade** | Order, OrderDelivery | jacy-dukang |
|
||||
| **benefit** | BenefitCoupon | jacy-dukang |
|
||||
| **redeem** | RedeemRecord, StoreRating | 刘京尧 |
|
||||
| **redeem** | RedeemRecord, StoreRating | 刘景尧 |
|
||||
| **settlement** | StorePayout, PartnerBill | jacy-dukang |
|
||||
| **ops** | 只读聚合 | jacy-dukang |
|
||||
| **analytics** | LogUserAnalytics | jacy-dukang |
|
||||
|
||||
@@ -34,7 +34,7 @@ export class RedeemService {
|
||||
});
|
||||
if (!coupon) throw new NotFoundException('券不存在');
|
||||
const balance = Number(coupon.balance);
|
||||
const check = validateRedeemAmount(balance, body.amount);
|
||||
const check = validateRedeemAmount(balance, body.amount, balance);
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
allocations = [{ couponId: coupon.id.toString(), amount: body.amount }];
|
||||
} else {
|
||||
@@ -115,7 +115,7 @@ export class RedeemService {
|
||||
where: { id: BigInt(alloc.couponId) },
|
||||
});
|
||||
if (!coupon) throw new BadRequestException('券不存在');
|
||||
const check = validateRedeemAmount(Number(coupon.balance), alloc.amount);
|
||||
const check = validateRedeemAmount(Number(coupon.balance), alloc.amount, Number(coupon.balance));
|
||||
if (!check.ok) throw new BadRequestException(check.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,8 +120,8 @@ Token 必须含 `actorType` + `actorId`(手册 §六 §1.5)。
|
||||
// packages/domain
|
||||
const benefitAmount = product.benefitAmount ?? product.price;
|
||||
|
||||
// 核销 PRD 上限 ¥500
|
||||
if (amount <= 0 || amount > coupon.balance || amount > 500) throw ...
|
||||
// V3 核销:直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额
|
||||
if (amount <= 0 || amount > allowedBalance) throw ...
|
||||
|
||||
// 订单 Tab
|
||||
// all | pending_pay | pending_ship | pending_receive | completed
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# 杜康好客 · V3 编码手册(交付业务版)
|
||||
|
||||
> **版本定位**:V3 是基于当前数据库 v3.1 的完整交付版本,目标不是 Mock 联调,而是把「购酒 → 发券 → 到店核销 → 门店打款 → 合伙人结算 → 总部运营」全流程跑通。
|
||||
> **对照文件**:V2 手册定义完整产品蓝图;preV1 手册定义 Mock 联调裁剪;本文件定义 V3 的交付口径、核销新规则、两人分工与业务闭环任务口径。
|
||||
> **数据库事实**:当前 Prisma schema 已是 v3.1,V3 不默认新增大表,优先补齐业务闭环、第三方集成、任务调度、验收测试与运营后台。
|
||||
|
||||
---
|
||||
|
||||
## 1. V3 交付目标
|
||||
|
||||
V3 必须达到可业务验收状态:
|
||||
|
||||
1. C 端用户能登录、选城、浏览商品、下单、支付、查看订单、获得权益、到店核销。
|
||||
2. 门店端能登录、扫码/输码核销、查看核销记录、管理营业状态,并形成待打款记录。
|
||||
3. 合伙人端能登录、录入门店、管理门店、查看辖区订单、处理配送/补发、查看账单与经营数据。
|
||||
4. WebAdmin 能完成开城、商品、门店审核、订单、权益、核销、配送、退款/补发、结算、资源和账号管理。
|
||||
5. 后端能完成真实支付回调、配送状态推进、退款/补发工单、门店 T+1、合伙人 T+30、日志与审计。
|
||||
6. 测试能覆盖主链路、关键边界和生产开关,不再只依赖一条 happy path 冒烟。
|
||||
|
||||
---
|
||||
|
||||
## 2. V3 端与负责人
|
||||
|
||||
| 负责人 | 主责端 | 主责后端/公共范围 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `jacy-dukang` | `apps/h5-user`、`apps/admin-web` | `packages/*`、`iam`、`catalog`、`trade`、`benefit`、`settlement`、`ops`、`callbacks`、`jobs`、`integrations`、Prisma | Tech lead,负责架构、主交易链路、支付退款、后台运营、交付验收 |
|
||||
| `刘景尧` | `apps/h5-shop`、`apps/h5-partner` | `store`、`redeem`,并配合 `settlement`、配送/核销联调 | 负责门店、合伙人、录店、核销、门店体验与辖区履约 |
|
||||
|
||||
协作规则:
|
||||
|
||||
- `apps/*` 只走 HTTP API 与 `packages/shared-types`,禁止 import `server/*` 或其他 app。
|
||||
- 后端跨模块只调用 exported Service,禁止为了赶进度直接写他人领域表。
|
||||
- 涉及 API、枚举、DTO、业务规则变更,必须同步 `packages/shared-types`、`packages/domain` 与本手册。
|
||||
- Prisma 迁移由 `jacy-dukang` 主导;涉及 `store` / `redeem` 表或核销流程时 `刘景尧` 必须 Review。
|
||||
|
||||
---
|
||||
|
||||
## 3. V3 核销规则(已替代 V2 的 ¥500 上限)
|
||||
|
||||
### 3.1 两种核销入口
|
||||
|
||||
| 入口 | 前端表现 | API 入参 | 限制规则 | 券扣减方式 |
|
||||
|---|---|---|---|---|
|
||||
| 直接点核销 | 用户在权益首页点击「去使用」 | `{ amount }`,不带 `couponId` | `0 < amount <= 用户全部 ACTIVE 权益总余额` | 按券创建时间 FIFO 扣减,可跨多张权益 |
|
||||
| 指向单据核销 | 用户在某张权益/核销单点击「立即核销」 | `{ couponId, amount }` | `0 < amount <= 该单据当前可用金额` | 只扣减该单据 |
|
||||
|
||||
### 3.2 后端不变量
|
||||
|
||||
- 核销码只存在 Redis,TTL = 5 分钟。
|
||||
- 生成核销码前必须校验金额,门店确认核销时必须二次校验。
|
||||
- 门店确认时使用券 `version` 乐观锁,避免并发重复扣减。
|
||||
- 核销成功后写入:
|
||||
- `user_redeem_record`
|
||||
- `common_event(BENEFIT_LEDGER, REDEEM)`
|
||||
- `store_payout(PENDING)`
|
||||
- 若核销码绑定门店,确认核销时只能由该门店使用;若未绑定门店,任意 `OPEN` 门店可确认。
|
||||
- 已关闭或暂停门店不可核销。
|
||||
|
||||
### 3.3 前端提示
|
||||
|
||||
- 直接核销:显示「最高可核销 = 当前好客权益总余额」。
|
||||
- 单据核销:显示「最高可核销 = 当前单据可用金额」。
|
||||
- 不再展示「单次最高可核销 ¥500.00」。
|
||||
|
||||
---
|
||||
|
||||
## 4. V3 完整业务闭环
|
||||
|
||||
### 4.1 C 端购酒与权益
|
||||
|
||||
1. 用户打开 H5,完成手机号/微信登录。
|
||||
2. 选择城市,首页展示已开城商品。
|
||||
3. 进入商品详情,选择数量与收货地址。
|
||||
4. 订单预览校验同城 2 瓶、跨城 6 瓶。
|
||||
5. 创建订单,状态 `PENDING_PAY`。
|
||||
6. 发起支付,Mock 环境同步成功,生产环境走微信 JSAPI。
|
||||
7. 支付成功回调幂等更新订单为 `PENDING_SHIP`。
|
||||
8. 根据商品 `benefitAmount ?? price` 发放好客权益。
|
||||
9. 用户在权益页直接核销或指定单据核销。
|
||||
10. 门店确认核销后,用户可评价,权益余额与流水更新。
|
||||
|
||||
### 4.2 门店核销与打款
|
||||
|
||||
1. 门店账号登录。
|
||||
2. 首页扫码或输入核销码。
|
||||
3. 后端校验核销码、门店状态、权益余额、单据金额。
|
||||
4. 核销成功生成记录。
|
||||
5. 系统创建 `store_payout(PENDING)`,预计 T+1 打款。
|
||||
6. 系统按门店绑定关系计算并记录对应合伙人的核销收益,用于合伙人账单与经营统计。
|
||||
7. WebAdmin 财务确认或 Job 自动推进打款状态。
|
||||
8. 门店端可查看核销记录与打款状态。
|
||||
9. 绑定合伙人端可查看辖区门店对应的核销订单、核销金额、门店打款状态与合伙人收益。
|
||||
|
||||
### 4.3 合伙人拓店与履约
|
||||
|
||||
1. 合伙人登录工作台。
|
||||
2. 录入门店资料、门头/环境图、合同资料、银行卡信息。
|
||||
3. V3 由 WebAdmin 审核门店,审核通过后门店才可对 C 端可见并参与核销。
|
||||
4. 合伙人查看辖区订单。
|
||||
5. 配送 Mock 或真实配送推进订单。
|
||||
6. 异常时发起/处理补发、改址拦截、配送异常。
|
||||
7. 合伙人查看月度账单、佣金、经营周报。
|
||||
|
||||
### 4.4 WebAdmin 运营
|
||||
|
||||
1. 管理员登录。
|
||||
2. 配置开城、商品、合伙人、门店分类。
|
||||
3. 审核门店。
|
||||
4. 查看订单与配送。
|
||||
5. 处理退款、补发、客服工单。
|
||||
6. 管理权益、核销、资源、账号。
|
||||
7. 财务确认门店 T+1 和合伙人 T+30 结算。
|
||||
8. 查看运营报表、异常预警、第三方日志。
|
||||
|
||||
---
|
||||
|
||||
## 5. V3 验收用例清单
|
||||
|
||||
### 必过主链路
|
||||
|
||||
1. C 端手机号登录成功。
|
||||
2. 首页展示郑州 4 个上架商品。
|
||||
3. 同城 1 瓶下单失败,2 瓶成功。
|
||||
4. 跨城 5 瓶下单失败,6 瓶成功。
|
||||
5. 支付成功后订单进入待发货,并发放权益。
|
||||
6. 直接核销不带 `couponId`,金额可达到总余额。
|
||||
7. 单据核销带 `couponId`,金额不能超过该单据余额。
|
||||
8. 门店扫码确认核销成功。
|
||||
9. 核销后生成 `store_payout(PENDING)`。
|
||||
10. 门店关闭后不可核销,C 端不可见关闭门店。
|
||||
11. 合伙人录店后进入审核流,审核通过后 C 端可见。
|
||||
12. 配送自动或真实回调推进到完成。
|
||||
13. 退款工单通过后订单/权益/第三方日志一致。
|
||||
14. 门店 T+1 打款状态可确认。
|
||||
15. 合伙人 T+30 账单可生成并确认。
|
||||
|
||||
### 必过后台链路
|
||||
|
||||
1. WebAdmin 登录成功。
|
||||
2. 创建/编辑/上下架商品。
|
||||
3. 审核门店。
|
||||
4. 查询订单与配送单。
|
||||
5. 查询权益券、核销记录、打款记录。
|
||||
6. 处理退款/补发/异常工单。
|
||||
7. 查看第三方日志与运营报表。
|
||||
8. 导出或核对财务数据。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前已知技术债
|
||||
|
||||
| 优先级 | 技术债 | 处理要求 |
|
||||
|---|---|---|
|
||||
| P0 | 旧文档与规则仍有 ¥500 上限描述 | V3 以后以本手册为准;后续批量清理 V2/preV1 中过时描述 |
|
||||
| P0 | `lint` 多数为 `echo ok` | 交付验收前必须接入有效检查 |
|
||||
| P0 | smoke 覆盖不足 | 按 4.x 业务闭环补齐主流程冒烟 |
|
||||
| P1 | shared-types DTO 不全 | 按接口稳定度分批上提 |
|
||||
| P1 | 跨模块直写 Prisma 表 | 逐步改为 exported Service |
|
||||
| P1 | 真实短信、配送、退款未闭环 | 按 4.1、4.3、4.4 对应业务闭环完成 |
|
||||
| P2 | `admin-web` 与 V2 HQ 小程序形态不一致 | V3 先以 WebAdmin 交付,是否迁小程序另立版本 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 版本冻结规则
|
||||
|
||||
- V3 业务规则以本文件为准。
|
||||
- V2 手册仍作为完整蓝图参考,但与 V3 冲突时,V3 优先。
|
||||
- preV1 手册只作为 Mock 联调历史参考,不再作为交付验收标准。
|
||||
- 未写入本文件的新增需求,不进入 V3 交付范围;如必须加入,先更新本手册与任务表负责人。
|
||||
Reference in New Issue
Block a user