From 309b1800873c89b22aab4940b2a3ab27fce6f497 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Tue, 28 Jul 2026 16:03:56 +0800 Subject: [PATCH] feat(admin): link city-partner store count; fix benefitUsageRule null string City partner storeCount jumps to /stores?partnerId=; normalize empty/null text so usage rule is not saved as literal "null. Co-authored-by: Cursor --- apps/admin-web/src/pages/CityPartnersPage.tsx | 17 ++++- apps/admin-web/src/pages/StoresPage.tsx | 67 +++++++++++++++---- apps/h5-partner/src/pages/StoreDetailPage.tsx | 5 +- .../src/pages/store-detail/index.tsx | 4 +- .../src/modules/ops/admin-stores.service.ts | 12 +++- .../src/modules/store/store.service.ts | 23 +++++-- 6 files changed, 103 insertions(+), 25 deletions(-) diff --git a/apps/admin-web/src/pages/CityPartnersPage.tsx b/apps/admin-web/src/pages/CityPartnersPage.tsx index 3a8dbbe..bcf7ab1 100644 --- a/apps/admin-web/src/pages/CityPartnersPage.tsx +++ b/apps/admin-web/src/pages/CityPartnersPage.tsx @@ -301,7 +301,16 @@ export default function CityPartnersPage() { ellipsis: true, render: (v) => v || '—', }, - { title: '门店', dataIndex: 'storeCount', width: 60 }, + { + title: '门店', + dataIndex: 'storeCount', + width: 60, + render: (n: number, row) => ( + + {n ?? 0} + + ), + }, { title: '子账号', dataIndex: 'accountCount', width: 70, render: (n) => Math.max(0, n - 1) }, { title: '创建', dataIndex: 'createdAt', width: 150, render: fmtTime }, { @@ -444,7 +453,11 @@ export default function CityPartnersPage() { <> {detail.cityName ?? '—'} - {detail.storeCount ?? 0} + + + {detail.storeCount ?? 0} + + {detail.managedWarehouseName ?? ( diff --git a/apps/admin-web/src/pages/StoresPage.tsx b/apps/admin-web/src/pages/StoresPage.tsx index 305da2a..128d7f1 100644 --- a/apps/admin-web/src/pages/StoresPage.tsx +++ b/apps/admin-web/src/pages/StoresPage.tsx @@ -324,12 +324,16 @@ export default function StoresPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const initialCityId = searchParams.get('cityId') ?? ''; + const initialPartnerId = searchParams.get('partnerId') ?? ''; const [form] = Form.useForm(); const [editForm] = Form.useForm(); const [createForm] = Form.useForm(); - const [filters, setFilters] = useState>(() => - initialCityId ? { cityId: initialCityId } : {}, - ); + const [filters, setFilters] = useState>(() => { + const init: Record = {}; + if (initialCityId) init.cityId = initialCityId; + if (initialPartnerId) init.partnerId = initialPartnerId; + return init; + }); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/stores', () => { @@ -339,11 +343,13 @@ export default function StoresPage() { if (filters.auditStatus) qs.set('auditStatus', filters.auditStatus); if (filters.phone) qs.set('phone', filters.phone); if (filters.cityId) qs.set('cityId', filters.cityId); + if (filters.partnerId) qs.set('partnerId', filters.partnerId); return qs; }, [filters], ); const [filterCities, setFilterCities] = useState([]); + const [filterPartners, setFilterPartners] = useState([]); const [detail, setDetail] = useState | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); const [rejectOpen, setRejectOpen] = useState(false); @@ -366,19 +372,34 @@ export default function StoresPage() { void request>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setFilterCities(res.items)) .catch(() => {}); + void request>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) + .then((res) => setFilterPartners(res.items)) + .catch(() => {}); }, []); useEffect(() => { const cityId = searchParams.get('cityId') ?? ''; + const partnerId = searchParams.get('partnerId') ?? ''; + let changed = false; setFilters((prev) => { - if ((prev.cityId ?? '') === cityId) return prev; const next = { ...prev }; - if (cityId) next.cityId = cityId; - else delete next.cityId; - return next; + if ((prev.cityId ?? '') !== cityId) { + changed = true; + if (cityId) next.cityId = cityId; + else delete next.cityId; + } + if ((prev.partnerId ?? '') !== partnerId) { + changed = true; + if (partnerId) next.partnerId = partnerId; + else delete next.partnerId; + } + return changed ? next : prev; }); - form.setFieldsValue({ cityId: cityId || undefined }); - if (cityId) setPage(1); + form.setFieldsValue({ + cityId: cityId || undefined, + partnerId: partnerId || undefined, + }); + if (cityId || partnerId) setPage(1); }, [searchParams, form, setPage]); const selectedPartnerId = Form.useWatch('partnerAccountId', createForm); @@ -452,7 +473,12 @@ export default function StoresPage() { // 以门店手机号为准保存;若与账号登录号不一致,保存时会强制同步到登录账号 phone: storePhone || loginPhone, intro: d.intro, - benefitUsageRule: d.benefitUsageRule, + benefitUsageRule: + d.benefitUsageRule != null && + String(d.benefitUsageRule).trim() && + !/^null$/i.test(String(d.benefitUsageRule).trim()) + ? String(d.benefitUsageRule) + : '', coverUrl: d.coverUrl, province: d.province, city: d.cityName, @@ -490,7 +516,12 @@ export default function StoresPage() { phone: v.phone, coverUrl: v.coverUrl, intro: v.intro, - benefitUsageRule: v.benefitUsageRule ?? null, + benefitUsageRule: + typeof v.benefitUsageRule === 'string' && + v.benefitUsageRule.trim() && + !/^null$/i.test(v.benefitUsageRule.trim()) + ? v.benefitUsageRule.trim() + : null, categoryId: v.categoryId, province: v.province, city: v.city, @@ -761,6 +792,16 @@ export default function StoresPage() { options={filterCities.map((c) => ({ value: c.id, label: c.name }))} /> + + ({ value, label }))} /> @@ -779,7 +820,9 @@ export default function StoresPage() { form.resetFields(); setFilters({}); setPage(1); - if (searchParams.has('cityId')) navigate('/stores', { replace: true }); + if (searchParams.has('cityId') || searchParams.has('partnerId')) { + navigate('/stores', { replace: true }); + } }} > 重置 diff --git a/apps/h5-partner/src/pages/StoreDetailPage.tsx b/apps/h5-partner/src/pages/StoreDetailPage.tsx index 8e83366..2aa3ace 100644 --- a/apps/h5-partner/src/pages/StoreDetailPage.tsx +++ b/apps/h5-partner/src/pages/StoreDetailPage.tsx @@ -67,7 +67,10 @@ export default function StoreDetailPage() { phone: String(data.phone || ''), address: String(data.address || ''), intro: String(data.intro || ''), - benefitUsageRule: String(data.benefitUsageRule || ''), + benefitUsageRule: (() => { + const raw = String(data.benefitUsageRule || '').trim(); + return raw && !/^null$/i.test(raw) ? raw : ''; + })(), latitude: data.latitude != null && data.latitude !== '' ? String(data.latitude) : '', longitude: data.longitude != null && data.longitude !== '' ? String(data.longitude) : '', }); diff --git a/apps/mini-user/src/pages/store-detail/index.tsx b/apps/mini-user/src/pages/store-detail/index.tsx index 3c0efec..35ca786 100644 --- a/apps/mini-user/src/pages/store-detail/index.tsx +++ b/apps/mini-user/src/pages/store-detail/index.tsx @@ -174,7 +174,9 @@ export default function StoreDetailPage() { ]); const intro = store.intro?.trim() || ''; - const benefitRule = store.benefitUsageRule?.trim() || ''; + const benefitRuleRaw = store.benefitUsageRule?.trim() || ''; + const benefitRule = + benefitRuleRaw && !/^null$/i.test(benefitRuleRaw) ? benefitRuleRaw : ''; function previewEnv(index: number) { if (!envPhotos.length) return; diff --git a/server/dukang-api/src/modules/ops/admin-stores.service.ts b/server/dukang-api/src/modules/ops/admin-stores.service.ts index 07f0a7c..417fe68 100644 --- a/server/dukang-api/src/modules/ops/admin-stores.service.ts +++ b/server/dukang-api/src/modules/ops/admin-stores.service.ts @@ -17,6 +17,14 @@ import type { UpdateStoreStatusDto, } from './dto/admin-mutate.dto'; +/** 选填文案:空 / null / "null" 一律存库为 null,避免 String(null)==="null" */ +function normalizeStoreOptionalText(value: unknown): string | null { + if (value == null) return null; + const s = String(value).trim(); + if (!s || /^null$/i.test(s) || /^undefined$/i.test(s)) return null; + return s; +} + @Injectable() export class AdminStoresService { constructor( @@ -286,7 +294,7 @@ export class AdminStoresService { ...(normalizedPhone !== undefined ? { phone: normalizedPhone } : {}), ...(dto.intro !== undefined ? { intro: dto.intro } : {}), ...(dto.benefitUsageRule !== undefined - ? { benefitUsageRule: String(dto.benefitUsageRule).trim() || null } + ? { benefitUsageRule: normalizeStoreOptionalText(dto.benefitUsageRule) } : {}), ...(dto.address !== undefined ? { address: dto.address.trim() } : {}), ...(dto.province !== undefined ? { province: dto.province.trim() } : {}), @@ -417,7 +425,7 @@ export class AdminStoresService { if (intro && (intro.length < 2 || intro.length > 500)) { throw new BadRequestException('门店简介须为 2~500 字'); } - const benefitUsageRule = dto.benefitUsageRule?.trim() || null; + const benefitUsageRule = normalizeStoreOptionalText(dto.benefitUsageRule); if (benefitUsageRule && benefitUsageRule.length > 1000) { throw new BadRequestException('好客权益券使用规则最多 1000 字'); } diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts index e38aed8..cb7ae83 100644 --- a/server/dukang-api/src/modules/store/store.service.ts +++ b/server/dukang-api/src/modules/store/store.service.ts @@ -28,6 +28,14 @@ function haversineMeters(lat1: number, lng1: number, lat2: number, lng2: number) return 2 * R * Math.asin(Math.min(1, Math.sqrt(a))); } +/** 选填文案:空 / null / "null" 存库为 null,避免 String(null)==="null" */ +function normalizeOptionalTextField(value: unknown): string | null { + if (value == null) return null; + const s = String(value).trim(); + if (!s || /^null$/i.test(s) || /^undefined$/i.test(s)) return null; + return s; +} + function parseOptionalCoord(value: unknown, kind: 'lat' | 'lng' = 'lng'): number | null { if (value == null || value === '') return null; const n = typeof value === 'number' ? value : Number(value); @@ -301,9 +309,8 @@ export class StoreService { if (introRaw && (introRaw.length < 2 || introRaw.length > 500)) { throw new BadRequestException('门店简介须为 2~500 字'); } - const benefitUsageRuleRaw = - body.benefitUsageRule != null ? String(body.benefitUsageRule).trim() : ''; - if (benefitUsageRuleRaw.length > 1000) { + const benefitUsageRuleRaw = normalizeOptionalTextField(body.benefitUsageRule); + if (benefitUsageRuleRaw && benefitUsageRuleRaw.length > 1000) { throw new BadRequestException('好客权益券使用规则最多 1000 字'); } @@ -325,7 +332,7 @@ export class StoreService { district: String(body.district ?? ''), address: String(body.address), intro: introRaw || null, - benefitUsageRule: benefitUsageRuleRaw || null, + benefitUsageRule: benefitUsageRuleRaw, avgPrice: avgPriceRaw, openTime, closeTime, @@ -522,7 +529,9 @@ export class StoreService { const address = body.address !== undefined ? String(body.address).trim() : undefined; const introRaw = body.intro !== undefined ? String(body.intro).trim() : undefined; const benefitUsageRuleRaw = - body.benefitUsageRule !== undefined ? String(body.benefitUsageRule).trim() : undefined; + body.benefitUsageRule !== undefined + ? normalizeOptionalTextField(body.benefitUsageRule) + : undefined; const latitude = body.latitude !== undefined ? parseOptionalCoord(body.latitude, 'lat') : undefined; const longitude = @@ -536,7 +545,7 @@ export class StoreService { if (introRaw && (introRaw.length < 2 || introRaw.length > 500)) { throw new BadRequestException('门店简介须为 2~500 字'); } - if (benefitUsageRuleRaw !== undefined && benefitUsageRuleRaw.length > 1000) { + if (benefitUsageRuleRaw != null && benefitUsageRuleRaw.length > 1000) { throw new BadRequestException('好客权益券使用规则最多 1000 字'); } if ( @@ -561,7 +570,7 @@ export class StoreService { ...(hasCoordsUpdate ? { latitude, longitude } : {}), ...(introRaw !== undefined ? { intro: introRaw || null } : {}), ...(benefitUsageRuleRaw !== undefined - ? { benefitUsageRule: benefitUsageRuleRaw || null } + ? { benefitUsageRule: benefitUsageRuleRaw } : {}), ...(resubmitAudit ? {