fix(promo): HQ 渠道负责人与关联合伙人展示为公司-人名

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 09:19:53 +08:00
parent b9921b1a75
commit 674b6ac31b
4 changed files with 34 additions and 32 deletions
@@ -0,0 +1,21 @@
/** HQ 推广码:渠道负责人 / 关联合伙人展示。有公司则「公司-人名」,无公司则人名。 */
export function formatPromoPartnerLabel(p?: {
companyName?: string | null;
name?: string | null;
id?: string;
} | null): string {
if (!p) return '—';
const company = (p.companyName ?? '').trim();
const person = (p.name ?? '').trim();
if (company && person) return `${company}-${person}`;
if (company) return company;
if (person) return person;
return p.id || '—';
}
export function formatPromoPartnerNames(
partners?: Array<{ companyName?: string | null; name?: string | null; id?: string }> | null,
): string {
if (!partners?.length) return '—';
return partners.map((p) => formatPromoPartnerLabel(p)).join('、');
}
+4 -15
View File
@@ -9,11 +9,11 @@ import {
PROMO_CODE_STATUS_LABELS, PROMO_CODE_STATUS_LABELS,
promoConversion, promoConversion,
type PromoCodeItem, type PromoCodeItem,
type PromoCodePartnerBrief,
type PromoCodeScene, type PromoCodeScene,
} from '@dukang/shared-types'; } from '@dukang/shared-types';
import { request, type Paginated } from '../lib/api'; import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { formatPromoPartnerLabel, formatPromoPartnerNames } from '../lib/promo-partner-label';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader'; import { AdminListHeader } from '../components/AdminListHeader';
@@ -23,17 +23,6 @@ type Row = PromoCodeItem;
type PartnerOption = { id: string; companyName?: string | null; name?: string; phone?: string }; type PartnerOption = { id: string; companyName?: string | null; name?: string; phone?: string };
type SceneOption = { value: PromoCodeScene; label: string }; type SceneOption = { value: PromoCodeScene; label: string };
function formatPartnerLabel(p?: PromoCodePartnerBrief | PartnerOption | null) {
if (!p) return '—';
const title = p.companyName || p.name || p.id;
return p.phone ? `${title} · ${p.phone}` : title;
}
function formatPartnerNames(partners?: PromoCodePartnerBrief[] | null) {
if (!partners?.length) return '—';
return partners.map((p) => p.companyName || p.name || p.id).join('、');
}
export default function PromoCodesPage() { export default function PromoCodesPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [filterForm] = Form.useForm(); const [filterForm] = Form.useForm();
@@ -50,7 +39,7 @@ export default function PromoCodesPage() {
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const partnerSelectOptions = useMemo( const partnerSelectOptions = useMemo(
() => partners.map((p) => ({ value: p.id, label: formatPartnerLabel(p) })), () => partners.map((p) => ({ value: p.id, label: formatPromoPartnerLabel(p) })),
[partners], [partners],
); );
@@ -142,12 +131,12 @@ export default function PromoCodesPage() {
{ {
title: '渠道负责人', title: '渠道负责人',
width: 160, width: 160,
render: (_, row) => formatPartnerNames(row.channelOwners), render: (_, row) => formatPromoPartnerNames(row.channelOwners),
}, },
{ {
title: '关联合伙人', title: '关联合伙人',
width: 140, width: 140,
render: (_, row) => row.assocPartner?.companyName || row.assocPartner?.name || '—', render: (_, row) => formatPromoPartnerLabel(row.assocPartner),
}, },
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
{ {
@@ -25,6 +25,7 @@ import {
} from '@dukang/shared-types'; } from '@dukang/shared-types';
import { request, type Paginated } from '../../lib/api'; import { request, type Paginated } from '../../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../../lib/constants'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../../lib/constants';
import { formatPromoPartnerLabel, formatPromoPartnerNames } from '../../lib/promo-partner-label';
import type { PromoCodeDetailContext } from './PromoCodeDetailLayout'; import type { PromoCodeDetailContext } from './PromoCodeDetailLayout';
import PromoCodeMetricsPanel from './PromoCodeMetricsPanel'; import PromoCodeMetricsPanel from './PromoCodeMetricsPanel';
@@ -53,17 +54,6 @@ const CONVERSION_HINT =
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string | null }; type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string | null };
function formatPartnerLabel(p?: { companyName?: string | null; name?: string | null; phone?: string | null; id: string } | null) {
if (!p) return '—';
const title = p.companyName || p.name || p.id;
return p.phone ? `${title} · ${p.phone}` : title;
}
function formatPartnerNames(partners?: Array<{ companyName?: string | null; name?: string | null; id: string }> | null) {
if (!partners?.length) return '—';
return partners.map((p) => p.companyName || p.name || p.id).join('、');
}
function StatHint({ label, hint }: { label: string; hint: string }) { function StatHint({ label, hint }: { label: string; hint: string }) {
return ( return (
<span> <span>
@@ -107,7 +97,7 @@ export default function PromoCodeDetailPage() {
const map = new Map(partners.map((p) => [p.id, p])); const map = new Map(partners.map((p) => [p.id, p]));
for (const p of detail.channelOwners ?? []) map.set(p.id, p); for (const p of detail.channelOwners ?? []) map.set(p.id, p);
if (detail.assocPartner) map.set(detail.assocPartner.id, detail.assocPartner); if (detail.assocPartner) map.set(detail.assocPartner.id, detail.assocPartner);
return [...map.values()].map((p) => ({ value: p.id, label: formatPartnerLabel(p) })); return [...map.values()].map((p) => ({ value: p.id, label: formatPromoPartnerLabel(p) }));
}, [partners, detail.channelOwners, detail.assocPartner]); }, [partners, detail.channelOwners, detail.assocPartner]);
useEffect(() => { useEffect(() => {
@@ -229,12 +219,12 @@ export default function PromoCodeDetailPage() {
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="渠道负责人"> <Descriptions.Item label="渠道负责人">
<span style={{ whiteSpace: 'nowrap' }}> <span style={{ whiteSpace: 'nowrap' }}>
{formatPartnerNames(detail.channelOwners)} {formatPromoPartnerNames(detail.channelOwners)}
</span> </span>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="关联合伙人"> <Descriptions.Item label="关联合伙人">
<span style={{ whiteSpace: 'nowrap' }}> <span style={{ whiteSpace: 'nowrap' }}>
{formatPartnerLabel(detail.assocPartner)} {formatPromoPartnerLabel(detail.assocPartner)}
</span> </span>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item> <Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
+4 -2
View File
@@ -2,6 +2,7 @@
> **2026-09-16 / 09-17** · promo / city-scope / store / admin-web / h5-partner / shared-types > **2026-09-16 / 09-17** · promo / city-scope / store / admin-web / h5-partner / shared-types
> **主题**:推广码渠道负责人(多选主合伙人)+ 关联合伙人(扫码 first-lock > **主题**:推广码渠道负责人(多选主合伙人)+ 关联合伙人(扫码 first-lock
> **2026-09-17 修订**HQ 渠道负责人 / 关联合伙人下拉与表格展示为「公司(如有)-人名」
--- ---
@@ -41,6 +42,7 @@ flowchart TD
- **关联合伙人**:可空、单选 ACTIVE 主合伙人。登录 touch 后 `tryBindIfUnbound`:无关联则写 `assoc_partner_account_id` + `assoc_bound_at`(不写 `assoc_sub_account_id`、不改 `sourceType`、不增加关联码 `assoc_scan_count`);已是同一人 noop;已是他人不抛错。只绑以后扫进来的用户。 - **关联合伙人**:可空、单选 ACTIVE 主合伙人。登录 touch 后 `tryBindIfUnbound`:无关联则写 `assoc_partner_account_id` + `assoc_bound_at`(不写 `assoc_sub_account_id`、不改 `sourceType`、不增加关联码 `assoc_scan_count`);已是同一人 noop;已是他人不抛错。只绑以后扫进来的用户。
- 两字段独立:只配渠道负责人 ≠ 绑用户;只配关联合伙人也会把用户写入该合伙人「关联用户」,并允许看三项汇总。 - 两字段独立:只配渠道负责人 ≠ 绑用户;只配关联合伙人也会把用户写入该合伙人「关联用户」,并允许看三项汇总。
- HQ 原 `ownerUserId` 列保留、创建/编辑不再暴露。 - HQ 原 `ownerUserId` 列保留、创建/编辑不再暴露。
- **展示**:渠道负责人、关联合伙人(筛选/创建/编辑下拉、列表列、详情)统一为「公司-人名」;无公司则只显示人名。多人用顿号拼接。
--- ---
@@ -79,7 +81,7 @@ flowchart TD
| API promo | `promo-code.service.ts``dto/promo-code.dto.ts``partner-promo-code.controller.ts``PromoModule` import `CityScopeModule` | | API promo | `promo-code.service.ts``dto/promo-code.dto.ts``partner-promo-code.controller.ts``PromoModule` import `CityScopeModule` |
| API city-scope | `PartnerCityService.tryBindIfUnbound` | | API city-scope | `PartnerCityService.tryBindIfUnbound` |
| API store | `PartnerAssocService.tryBindIfUnbound` 转调 city-scope | | API store | `PartnerAssocService.tryBindIfUnbound` 转调 city-scope |
| admin-web | `PromoCodesPage.tsx``promo/PromoCodeDetailPage.tsx`:渠道负责人多选 / 关联合伙人单选;去掉关联用户 ID | | admin-web | `PromoCodesPage.tsx``promo/PromoCodeDetailPage.tsx`:渠道负责人多选 / 关联合伙人单选;去掉关联用户 ID;展示「公司-人名」(`lib/promo-partner-label.ts` |
| h5-partner | `CenterPage.tsx` 运营管理「推广码数据」;`/center/promo-codes``PromoCodesPage.tsx` | | h5-partner | `CenterPage.tsx` 运营管理「推广码数据」;`/center/promo-codes``PromoCodesPage.tsx` |
| 本地代理 | admin/partner/shop Vite 默认 `VITE_API_TARGET``http://127.0.0.1:3010`Windows 上 `localhost` 可能打到占用 `::3010` 的其他进程) | | 本地代理 | admin/partner/shop Vite 默认 `VITE_API_TARGET``http://127.0.0.1:3010`Windows 上 `localhost` 可能打到占用 `::3010` 的其他进程) |
@@ -87,7 +89,7 @@ flowchart TD
## 5. 验收 ## 5. 验收
- [ ] HQ 创建/编辑推广码可多选渠道负责人、可清空单选关联合伙人;列表/详情展示公司;可按两字段筛选 - [ ] HQ 创建/编辑推广码可多选渠道负责人、可清空单选关联合伙人;下拉与列表/详情展示公司-人名」(无公司则人名);可按两字段筛选
- [ ] 只配渠道负责人:扫码不写用户关联;该主账号 H5「推广码数据」能看到三项数字,点不开用户/订单/核销 - [ ] 只配渠道负责人:扫码不写用户关联;该主账号 H5「推广码数据」能看到三项数字,点不开用户/订单/核销
- [ ] 只配关联合伙人:未关联的登录用户扫码后出现在该合伙人「关联用户」;该主账号也能看三项汇总 - [ ] 只配关联合伙人:未关联的登录用户扫码后出现在该合伙人「关联用户」;该主账号也能看三项汇总
- [ ] 用户已关联他人:扫带关联合伙人的码不换绑、流程不中断、不报「无法更换」 - [ ] 用户已关联他人:扫带关联合伙人的码不换绑、流程不中断、不报「无法更换」