diff --git a/.cursor/skills/dukang-coding/SKILL.md b/.cursor/skills/dukang-coding/SKILL.md index b2b2417..58e1c3c 100644 --- a/.cursor/skills/dukang-coding/SKILL.md +++ b/.cursor/skills/dukang-coding/SKILL.md @@ -89,7 +89,7 @@ await this.prisma.order.update({ ... }); ```typescript const benefitAmount = product.benefitAmount ?? product.price; // 核销:直接核销按全部 ACTIVE 权益总余额;带单据核销按该单据可用金额 -// 同城 min 2 瓶 / 跨城 min 6 瓶 +// 起购:提货/同城 min 2 瓶 / 跨城 min 6 瓶(城市配置,非承运商) // 支付成功 → log_third_party + user_order.pay_status=PAID // → user_benefit_coupon + common_event(BENEFIT_LEDGER, GRANT) diff --git a/.cursor/skills/dukang-v3/SKILL.md b/.cursor/skills/dukang-v3/SKILL.md index 13de81f..4ea8f56 100644 --- a/.cursor/skills/dukang-v3/SKILL.md +++ b/.cursor/skills/dukang-v3/SKILL.md @@ -37,7 +37,7 @@ disable-model-invocation: true ``` 权益额 = 订单实付 1:1(永久) -同城起购 2 瓶 / 跨城 1 箱(6瓶) +起购:现场提货 2 瓶 / 同城 2 瓶 / 跨城 1 箱(6瓶);属城市,非承运商 门店结算 = 核销额 × 60% 单合伙人:订单佣金 + 核销佣金 ≤ 5%(默认 0% + 3%) 核销码 TTL = 3 分钟;试核销固定 ¥100 diff --git a/.cursor/skills/dukang-v3/reference-req-index.md b/.cursor/skills/dukang-v3/reference-req-index.md index 0715e2c..c5636b0 100644 --- a/.cursor/skills/dukang-v3/reference-req-index.md +++ b/.cursor/skills/dukang-v3/reference-req-index.md @@ -12,9 +12,9 @@ | REQ-U-004 | 定位 | 右上角市区;默认 IP;可授权精确定位 | | REQ-U-005 | 首页商品 | 4 款酒祖杜康;大图、标题加粗、价格标红、权益金额 | | REQ-U-006 | 商品详情 | 规格瓶/箱、度数、价格、权益、详情图 | -| REQ-U-007 | 同城 | 小飞侠;≥2 瓶免运费;物流节点+拍照;24h 自动完成 | -| REQ-U-008 | 现场提货 | 隐藏入口;支付即完成;强提示在现场 | -| REQ-U-009 | 跨城 | 未开通城市;≥1 箱;总部物流到付 | +| REQ-U-007 | 同城 | 小飞侠;起购 ≥2 瓶(城市 `localMinQty`);≥2 瓶免运费;物流节点+拍照;24h 自动完成 | +| REQ-U-008 | 现场提货 | 隐藏入口;起购 ≥2 瓶(城市 `pickupMinQty`);支付即完成;强提示在现场 | +| REQ-U-009 | 跨城 | 未开通城市;起购 ≥1 箱(城市 `crossMinQty`=6 瓶当量);总部物流到付 | | REQ-U-010 | 锁单 | 待付款 30 分钟取消 | | REQ-U-011 | 权益 | 支付成功 1:1 永久 | | REQ-U-012 | 客服 | 已付款起电话/微信客服 | diff --git a/AGENTS.md b/AGENTS.md index be10f31..ab08559 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ ``` 权益额 = benefit_amount ?? price -同城起购 2 瓶 / 跨城 6 瓶 +起购:现场提货 2 瓶 / 同城 2 瓶 / 跨城 6 瓶(1 箱);属城市,非承运商 核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额;Redis 码 3 分钟 C 端门店仅 status=OPEN 订单 Tab:待付款 | 已付款 | 已完成 diff --git a/agent.md b/agent.md index ff0be89..8d3240b 100644 --- a/agent.md +++ b/agent.md @@ -92,7 +92,7 @@ ```text 权益发放额 = common_product_item.benefit_amount ?? price -同城起购 2 瓶 / 跨城 6 瓶 +起购:现场提货 2 瓶 / 同城 2 瓶 / 跨城 6 瓶(1 箱);属城市,非承运商 核销:直接核销 0 < amount ≤ 全部 ACTIVE 权益总余额;带单据核销 0 < amount ≤ 该单据可用金额 C 端门店列表仅 status=OPEN 订单 Tab:all | pending_pay | pending_ship | pending_receive | completed diff --git a/apps/admin-web/src/components/ProductSpecsEditor.tsx b/apps/admin-web/src/components/ProductSpecsEditor.tsx index b89901b..16201f1 100644 --- a/apps/admin-web/src/components/ProductSpecsEditor.tsx +++ b/apps/admin-web/src/components/ProductSpecsEditor.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Button, Image, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Typography, message, } from 'antd'; @@ -50,6 +50,38 @@ function keyOf(ids: string[]) { return [...ids].sort().join('_'); } +function normalizeSkuRows(initialSkus: SkuRow[]): SkuRow[] { + if (!initialSkus.length) { + return [ + { + specValueIds: [], + barcode69: '', + price: 0, + status: 'ON_SALE', + allowOnlinePurchase: true, + allowCrossCityDelivery: true, + allowOnSitePickup: false, + saleUnit: 'BOTTLE', + bottlesPerUnit: 1, + isDefault: true, + imageUrl: '', + }, + ]; + } + return initialSkus.map((s) => ({ + ...s, + specValueIds: s.specValueIds ?? [], + status: s.status || 'ON_SALE', + allowOnlinePurchase: s.allowOnlinePurchase !== false, + allowCrossCityDelivery: s.allowCrossCityDelivery !== false, + allowOnSitePickup: !!s.allowOnSitePickup, + saleUnit: s.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE', + bottlesPerUnit: s.bottlesPerUnit || (s.saleUnit === 'BOX' ? 6 : 1), + isDefault: !!s.isDefault, + imageUrl: s.imageUrl ?? '', + })); +} + type Props = { productId: string; initialAttrs: SpecAttr[]; @@ -58,44 +90,19 @@ type Props = { }; export default function ProductSpecsEditor({ productId, initialAttrs, initialSkus, onSaved }: Props) { - const [attrs, setAttrs] = useState( - initialAttrs.length - ? initialAttrs - : [], - ); - const [skus, setSkus] = useState( - initialSkus.length - ? initialSkus.map((s) => ({ - ...s, - specValueIds: s.specValueIds ?? [], - status: s.status || 'ON_SALE', - allowOnlinePurchase: s.allowOnlinePurchase !== false, - allowCrossCityDelivery: s.allowCrossCityDelivery !== false, - allowOnSitePickup: !!s.allowOnSitePickup, - saleUnit: s.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE', - bottlesPerUnit: s.bottlesPerUnit || (s.saleUnit === 'BOX' ? 6 : 1), - isDefault: !!s.isDefault, - imageUrl: s.imageUrl ?? '', - })) - : [ - { - specValueIds: [], - barcode69: '', - price: 0, - status: 'ON_SALE', - allowOnlinePurchase: true, - allowCrossCityDelivery: true, - allowOnSitePickup: false, - saleUnit: 'BOTTLE', - bottlesPerUnit: 1, - isDefault: true, - imageUrl: '', - }, - ], - ); + const [attrs, setAttrs] = useState(() => (initialAttrs.length ? initialAttrs : [])); + const [skus, setSkus] = useState(() => normalizeSkuRows(initialSkus)); const [saving, setSaving] = useState(false); const [editIndex, setEditIndex] = useState(null); + useEffect(() => { + setAttrs(initialAttrs.length ? initialAttrs : []); + setSkus(normalizeSkuRows(initialSkus)); + setEditIndex(null); + // 仅切商品时重载,避免父组件重渲染冲掉编辑中内容 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [productId]); + const combos = useMemo(() => cartesian(attrs.filter((a) => a.name.trim() && a.values.some((v) => v.name.trim()))), [attrs]); function regenerateMatrix() { diff --git a/apps/admin-web/src/pages/CitiesPage.tsx b/apps/admin-web/src/pages/CitiesPage.tsx index b1dc8ff..883e67d 100644 --- a/apps/admin-web/src/pages/CitiesPage.tsx +++ b/apps/admin-web/src/pages/CitiesPage.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import { Button, Descriptions, + Divider, Drawer, Form, Input, @@ -362,8 +363,28 @@ export default function CitiesPage() {