diff --git a/apps/admin-web/src/components/CityPartnersPanel.tsx b/apps/admin-web/src/components/CityPartnersPanel.tsx index f7537fd..1e8186e 100644 --- a/apps/admin-web/src/components/CityPartnersPanel.tsx +++ b/apps/admin-web/src/components/CityPartnersPanel.tsx @@ -26,7 +26,7 @@ import { type PartnerPermissionKey, } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; -import { districtCodeLabel } from '../lib/china-region'; +import { districtCodeLabel, formatDistrictLabels } from '../lib/china-region'; import { fmtTime } from '../lib/constants'; import CityDistrictMultiSelect from './CityDistrictMultiSelect'; import PartnerSubAccountList from './PartnerSubAccountList'; @@ -37,6 +37,7 @@ type PartnerRow = { phone: string; name: string; scopeType?: string; + districtCodes?: string[] | null; orderCommissionRate?: number; redeemCommissionRate?: number; accountCount: number; @@ -211,6 +212,14 @@ export default function CityPartnersPanel({ const columns: ColumnsType = [ { title: '公司名', dataIndex: 'companyName', ellipsis: true }, + { + title: '区县', + dataIndex: 'districtCodes', + width: 160, + ellipsis: true, + render: (codes: string[] | null | undefined, row) => + row.scopeType === CityPartnerScopeType.CITY_WIDE ? '全城' : formatDistrictLabels(codes), + }, { title: '主账号', dataIndex: 'phone', width: 120 }, { title: '管辖', @@ -287,7 +296,7 @@ export default function CityPartnersPanel({ @@ -386,7 +395,7 @@ export default function CityPartnersPanel({ name="districtCodes" label="区县" rules={[{ required: true, message: '请选择至少一个区县' }]} - extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合" + extra="仅作标识,可多选当前城市下的区县(不做互斥)" > diff --git a/apps/admin-web/src/lib/china-region.ts b/apps/admin-web/src/lib/china-region.ts index 6612952..5bbc04e 100644 --- a/apps/admin-web/src/lib/china-region.ts +++ b/apps/admin-web/src/lib/china-region.ts @@ -106,6 +106,12 @@ export function districtCodeLabel(code: string): string { return codeToText[code] || code; } +/** 列表展示:区县码 → 中文名,顿号拼接 */ +export function formatDistrictLabels(codes?: string[] | null): string { + if (!codes?.length) return '—'; + return codes.map((c) => districtCodeLabel(String(c))).join('、'); +} + export function parseRegionCodes(codes?: string[]): ParsedChinaRegion | null { if (!codes || codes.length < 3) return null; const [provinceCode, cityCode, districtCode] = codes; diff --git a/apps/admin-web/src/pages/CityPartnersPage.tsx b/apps/admin-web/src/pages/CityPartnersPage.tsx index af4671e..a586087 100644 --- a/apps/admin-web/src/pages/CityPartnersPage.tsx +++ b/apps/admin-web/src/pages/CityPartnersPage.tsx @@ -31,7 +31,7 @@ import { type PartnerPermissionKey, } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; -import { districtCodeLabel } from '../lib/china-region'; +import { districtCodeLabel, formatDistrictLabels } from '../lib/china-region'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect'; @@ -48,6 +48,7 @@ type Row = { cityId?: string | null; cityName?: string | null; scopeType?: string; + districtCodes?: string[] | null; orderCommissionRate?: number; redeemCommissionRate?: number; bindingStatus?: string; @@ -259,7 +260,15 @@ export default function CityPartnersPage() { } const columns: ColumnsType = [ - { title: '公司名', dataIndex: 'companyName', ellipsis: true }, + { title: '公司名', dataIndex: 'companyName', ellipsis: true, width: 140 }, + { + title: '区县', + dataIndex: 'districtCodes', + width: 160, + ellipsis: true, + render: (codes: string[] | null | undefined, row) => + row.scopeType === CityPartnerScopeType.CITY_WIDE ? '全城' : formatDistrictLabels(codes), + }, { title: '城市', dataIndex: 'cityName', width: 90 }, { title: '主账号姓名', dataIndex: 'name', width: 100, ellipsis: true }, { title: '登录手机', dataIndex: 'phone', width: 120 }, @@ -476,7 +485,7 @@ export default function CityPartnersPage() { @@ -591,7 +600,7 @@ export default function CityPartnersPage() { name="districtCodes" label="区县" rules={[{ required: true, message: '请选择至少一个区县' }]} - extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合" + extra="仅作标识,可多选当前城市下的区县(不做互斥)" > diff --git a/apps/admin-web/src/pages/PartnersPage.tsx b/apps/admin-web/src/pages/PartnersPage.tsx index 170cec2..e28345f 100644 --- a/apps/admin-web/src/pages/PartnersPage.tsx +++ b/apps/admin-web/src/pages/PartnersPage.tsx @@ -23,7 +23,7 @@ import { type PartnerPermissionKey, } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; -import { districtCodeLabel } from '../lib/china-region'; +import { districtCodeLabel, formatDistrictLabels } from '../lib/china-region'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect'; @@ -37,6 +37,7 @@ type Row = { cityId?: string | null; cityName?: string | null; scopeType?: string; + districtCodes?: string[] | null; orderCommissionRate?: number; redeemCommissionRate?: number; storeCount: number; @@ -174,6 +175,14 @@ export default function PartnersPage() { const columns: ColumnsType = [ { title: '公司名', dataIndex: 'companyName', ellipsis: true }, + { + title: '区县', + dataIndex: 'districtCodes', + width: 160, + ellipsis: true, + render: (codes: string[] | null | undefined, row) => + row.scopeType === CityPartnerScopeType.CITY_WIDE ? '全城' : formatDistrictLabels(codes), + }, { title: '城市', dataIndex: 'cityName', width: 100 }, { title: '主账号', dataIndex: 'phone', width: 130 }, { @@ -251,7 +260,7 @@ export default function PartnersPage() { @@ -351,7 +360,7 @@ export default function PartnersPage() { name="districtCodes" label="区县" rules={[{ required: true, message: '请选择至少一个区县' }]} - extra="仅可选当前开城城市下的区县;不可与其他区域合伙人重合" + extra="仅作标识,可多选当前城市下的区县(不做互斥)" > diff --git a/packages/domain/src/city-partner.test.ts b/packages/domain/src/city-partner.test.ts index 17e3efe..1ced97d 100644 --- a/packages/domain/src/city-partner.test.ts +++ b/packages/domain/src/city-partner.test.ts @@ -61,7 +61,7 @@ describe('validatePartnerCityBinding', () => { expect(result.ok).toBe(false); }); - it('rejects overlapping district codes', () => { + it('allows overlapping district codes as labels', () => { const result = validatePartnerCityBinding( [ { @@ -74,10 +74,7 @@ describe('validatePartnerCityBinding', () => { ], { partnerAccountId: '11', scopeType: 'DISTRICT', districtCodes: ['410105'] }, ); - expect(result.ok).toBe(false); - expect(result.message).toContain('区域重合'); - expect(result.message).toContain('甲公司'); - expect(result.occupiedCodes).toEqual(['410105']); + expect(result.ok).toBe(true); }); it('allows valid district binding', () => { diff --git a/packages/domain/src/city-partner.ts b/packages/domain/src/city-partner.ts index 8dbdb8d..ee91785 100644 --- a/packages/domain/src/city-partner.ts +++ b/packages/domain/src/city-partner.ts @@ -22,7 +22,6 @@ export interface PartnerCityResolveRef { export interface PartnerCityValidationResult { ok: boolean; message?: string; - occupiedCodes?: string[]; } function normalizeDistrictCodes(codes?: string[] | null): string[] { @@ -101,27 +100,7 @@ export function validatePartnerCityBinding( return { ok: false, message: '区域合伙人须至少选择一个区县' }; } - const occupiedBy = new Map(); - for (const row of others) { - if (row.scopeType !== 'DISTRICT') continue; - const owner = row.companyName?.trim() || '其他区域合伙人'; - for (const code of normalizeDistrictCodes(row.districtCodes)) { - if (!occupiedBy.has(code)) occupiedBy.set(code, owner); - } - } - - const overlaps = districts.filter((code) => occupiedBy.has(code)); - if (overlaps.length) { - const detail = overlaps - .map((code) => `${code}(${occupiedBy.get(code)})`) - .join('、'); - return { - ok: false, - message: `区域重合:以下区县已被占用,请改选其他区县 — ${detail}`, - occupiedCodes: overlaps, - }; - } - + // 区县仅为标识,允许多个区域合伙人选择相同区县 return { ok: true }; } diff --git a/server/dukang-api/src/modules/ops/admin-partners.service.ts b/server/dukang-api/src/modules/ops/admin-partners.service.ts index 759e329..9c87a5b 100644 --- a/server/dukang-api/src/modules/ops/admin-partners.service.ts +++ b/server/dukang-api/src/modules/ops/admin-partners.service.ts @@ -80,6 +80,7 @@ export class AdminPartnersService { maxPartnerCommissionRate: p.city?.maxPartnerCommissionRate != null ? Number(p.city.maxPartnerCommissionRate) : null, scopeType: p.scopeType, + districtCodes: this.partnerCityService.parseDistrictCodes(p.districtCodes), orderCommissionRate: Number(p.orderCommissionRate ?? 0), redeemCommissionRate: Number(p.redeemCommissionRate ?? 0.03), bindingStatus: p.bindingStatus,