From 3c19b7dd9708d8268cc325d059cd4b86242bccf2 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Mon, 3 Aug 2026 19:30:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(store):=20=E9=97=A8=E5=BA=97=E5=A5=97?= =?UTF-8?q?=E9=A4=90=20v3.4.10=20=E5=85=A8=E7=AB=AF=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 StorePackage 与变更审核流,覆盖总部直存、合伙/门店提审、C 端展示与套餐异议工单;同步 PRD/开发文档与 REQ 索引。 Co-authored-by: Cursor --- .../skills/dukang-v3/reference-req-index.md | 4 + apps/admin-web/src/App.tsx | 2 + .../components/AdminStorePackagesSection.tsx | 154 ++++++++ .../src/pages/StorePackageAuditsPage.tsx | 148 +++++++ apps/admin-web/src/pages/StoresPage.tsx | 6 + apps/admin-web/src/pages/TicketsPage.tsx | 1 + apps/h5-partner/src/PartnerAppRoutes.tsx | 3 + .../src/components/StorePackagesForm.tsx | 104 +++++ apps/h5-partner/src/lib/storeDraft.ts | 13 + apps/h5-partner/src/lib/storePackages.ts | 49 +++ apps/h5-partner/src/pages/StoreCreatePage.tsx | 69 +++- apps/h5-partner/src/pages/StoreDetailPage.tsx | 12 + .../src/pages/StorePackagesPage.tsx | 113 ++++++ apps/h5-shop/src/App.tsx | 3 +- apps/h5-shop/src/lib/storePackages.ts | 43 ++ apps/h5-shop/src/pages/MinePage.tsx | 6 + apps/h5-shop/src/pages/PackagesPage.tsx | 172 ++++++++ .../src/pages/store-detail/index.tsx | 86 +++- apps/mini-user/src/styles/store-detail.css | 82 ++++ packages/shared-types/src/index.ts | 1 + packages/shared-types/src/store-package.ts | 60 +++ packages/shared-types/src/ticket.ts | 7 +- server/dukang-api/prisma/schema.prisma | 51 +++ .../modules/common/dto/common-mutate.dto.ts | 4 +- .../modules/store/store-package.controller.ts | 104 +++++ .../modules/store/store-package.service.ts | 315 +++++++++++++++ .../src/modules/store/store.module.ts | 17 +- .../src/modules/store/store.service.ts | 12 + .../src/modules/trade/dto/after-sale.dto.ts | 17 +- .../src/modules/trade/trade.controller.ts | 13 +- .../src/modules/trade/trade.module.ts | 2 + .../src/modules/trade/trade.service.ts | 34 ++ 杜康好客-v3-PRD.md | 65 ++- 杜康好客-v3-现状对照.md | 12 + 杜康好客-门店套餐功能开发文档-v3.4.10.md | 372 ++++++++++++++++++ 35 files changed, 2128 insertions(+), 28 deletions(-) create mode 100644 apps/admin-web/src/components/AdminStorePackagesSection.tsx create mode 100644 apps/admin-web/src/pages/StorePackageAuditsPage.tsx create mode 100644 apps/h5-partner/src/components/StorePackagesForm.tsx create mode 100644 apps/h5-partner/src/lib/storePackages.ts create mode 100644 apps/h5-partner/src/pages/StorePackagesPage.tsx create mode 100644 apps/h5-shop/src/lib/storePackages.ts create mode 100644 apps/h5-shop/src/pages/PackagesPage.tsx create mode 100644 packages/shared-types/src/store-package.ts create mode 100644 server/dukang-api/src/modules/store/store-package.controller.ts create mode 100644 server/dukang-api/src/modules/store/store-package.service.ts create mode 100644 杜康好客-门店套餐功能开发文档-v3.4.10.md diff --git a/.cursor/skills/dukang-v3/reference-req-index.md b/.cursor/skills/dukang-v3/reference-req-index.md index 6e9287c..0715e2c 100644 --- a/.cursor/skills/dukang-v3/reference-req-index.md +++ b/.cursor/skills/dukang-v3/reference-req-index.md @@ -32,6 +32,7 @@ | REQ-U-024 | 推广码 | 扫码归因;现场提货有码归码 | | REQ-U-025 | 分享 | 微信商品分享+渠道参数 | | REQ-U-026 | 门店规则 | 附件一完整展示 | +| REQ-U-027 | 门店套餐 **3.4.10** | 门店详情展示生效套餐;套餐异议申诉入口 | ## 门店端 REQ-S @@ -58,6 +59,7 @@ | REQ-S-018 | 权限 | §4.6 矩阵 | | REQ-S-019 | 多店选店 | 记住上次所选 | | REQ-S-020 | 待处理核销 | Wave 3 弱网单 | +| REQ-S-021 | 门店套餐 **3.4.10** | 套餐列表/编辑/提审 | ## 合伙人端 REQ-P @@ -89,6 +91,7 @@ | REQ-P-024 | 负责人复核 | 待复核→待总部审 | | REQ-P-025 | 合同同日 | 同日提交校验 | | REQ-P-026 | 我的仓库 | Wave 3 管仓只读 | +| REQ-P-027 | 门店套餐 **3.4.10** | 拓店套餐页;门店套餐列表/编辑/提审 | ## 总部端 REQ-H @@ -122,3 +125,4 @@ | REQ-H-022 | 签约主体 | 山西领势酒业 | | REQ-H-023 | 热力图 | 腾讯位置服务 | | REQ-H-024 | 结算异议 | 门店 3 日异议处理 | +| REQ-H-025 | 门店套餐 **3.4.10** | 门店详情套餐 Tab 直存;套餐变更审核通过/驳回 | diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 4f5adba..49051bd 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -5,6 +5,7 @@ import LoginPage from './pages/LoginPage'; import DashboardPage from './pages/DashboardPage'; import UsersPage from './pages/UsersPage'; import OrdersPage from './pages/OrdersPage'; +import StorePackageAuditsPage from './pages/StorePackageAuditsPage'; import StoresPage from './pages/StoresPage'; import StoreRatingsPage from './pages/StoreRatingsPage'; import StoreAccountsPage from './pages/StoreAccountsPage'; @@ -79,6 +80,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin-web/src/components/AdminStorePackagesSection.tsx b/apps/admin-web/src/components/AdminStorePackagesSection.tsx new file mode 100644 index 0000000..9a38790 --- /dev/null +++ b/apps/admin-web/src/components/AdminStorePackagesSection.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react'; +import { Button, Space, Typography, message } from 'antd'; +import type { StorePackageItemDto } from '@dukang/shared-types'; +import { STORE_PACKAGE_MAX_COUNT } from '@dukang/shared-types'; +import { request } from '../lib/api'; + +type PackageRow = StorePackageItemDto; + +function emptyRow(index = 0): PackageRow { + return { name: '', price: '0', dishes: '', usableTime: '', otherNotes: '', sortOrder: index }; +} + +export default function AdminStorePackagesSection({ storeId }: { storeId: string }) { + const [items, setItems] = useState([emptyRow()]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + setLoading(true); + request<{ live: PackageRow[] }>(`/admin/stores/${storeId}/packages`) + .then((data) => { + setItems( + data.live?.length + ? data.live.map((p, i) => ({ ...p, price: String(p.price), sortOrder: i })) + : [emptyRow()], + ); + }) + .catch((e) => message.error(e instanceof Error ? e.message : '加载套餐失败')) + .finally(() => setLoading(false)); + }, [storeId]); + + function updateAt(index: number, patch: Partial) { + setItems((prev) => prev.map((item, i) => (i === index ? { ...item, ...patch } : item))); + } + + function addRow() { + if (items.length >= STORE_PACKAGE_MAX_COUNT) return; + setItems((prev) => [...prev, emptyRow(prev.length)]); + } + + function removeAt(index: number) { + setItems((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, sortOrder: i }))); + } + + async function save() { + const filled = items + .map((item, index) => ({ + name: item.name.trim(), + price: item.price.trim(), + dishes: item.dishes.trim(), + usableTime: item.usableTime?.trim() || null, + otherNotes: item.otherNotes?.trim() || null, + sortOrder: index, + })) + .filter((item) => item.name || item.dishes || item.price); + + for (let i = 0; i < filled.length; i++) { + const item = filled[i]; + if (!item.name) { + message.warning(`第 ${i + 1} 条套餐名称不能为空`); + return; + } + if (!item.dishes) { + message.warning(`第 ${i + 1} 条套餐菜品不能为空`); + return; + } + const price = Number(item.price); + if (!Number.isFinite(price) || price < 0) { + message.warning(`第 ${i + 1} 条套餐价格须为非负数字`); + return; + } + } + + setSaving(true); + try { + await request(`/admin/stores/${storeId}/packages`, { + method: 'PUT', + body: JSON.stringify({ + packages: filled.map((p) => ({ ...p, price: Number(p.price).toFixed(2) })), + }), + }); + message.success('套餐已保存并生效'); + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setSaving(false); + } + } + + if (loading) return 加载套餐中…; + + return ( +
+ + 总部直存立即生效,无需审核。最多 {STORE_PACKAGE_MAX_COUNT} 条。 + + {items.map((item, index) => ( +
+ + 套餐 {index + 1} + {items.length > 1 ? ( + + ) : null} + + + updateAt(index, { name: e.target.value })} + style={{ width: '100%', padding: 8 }} + /> + updateAt(index, { price: e.target.value })} + style={{ width: '100%', padding: 8 }} + /> +