城市合伙人端的修改(后台)

This commit is contained in:
2026-07-12 10:19:39 +08:00
parent d0f0fa09af
commit 7f031cc4c2
74 changed files with 5430 additions and 975 deletions
+96
View File
@@ -0,0 +1,96 @@
import { describe, expect, it } from 'vitest';
import {
resolveOrderCityPartner,
validatePartnerCityBinding,
validatePartnerCommissionRates,
} from './city-partner';
describe('resolveOrderCityPartner', () => {
const bindings = [
{
id: '1',
partnerAccountId: '10',
scopeType: 'CITY_WIDE' as const,
districtCodes: null,
orderCommissionRate: 0,
redeemCommissionRate: 0.03,
bindingStatus: 'ACTIVE' as const,
},
{
id: '2',
partnerAccountId: '20',
scopeType: 'DISTRICT' as const,
districtCodes: ['410105', '金水区'],
orderCommissionRate: 0.01,
redeemCommissionRate: 0.03,
bindingStatus: 'ACTIVE' as const,
},
];
it('prefers district partner when district matches adcode', () => {
const ref = resolveOrderCityPartner(bindings, '410105');
expect(ref?.partnerAccountId).toBe('20');
expect(ref?.scopeType).toBe('DISTRICT');
});
it('falls back to city-wide partner', () => {
const ref = resolveOrderCityPartner(bindings, '410102');
expect(ref?.partnerAccountId).toBe('10');
expect(ref?.scopeType).toBe('CITY_WIDE');
});
it('returns null when no bindings', () => {
expect(resolveOrderCityPartner([], '410105')).toBeNull();
});
});
describe('validatePartnerCityBinding', () => {
it('rejects duplicate partner', () => {
const result = validatePartnerCityBinding(
[{ id: '1', partnerAccountId: '10', scopeType: 'CITY_WIDE' }],
{ partnerAccountId: '10', scopeType: 'DISTRICT', districtCodes: ['410105'] },
);
expect(result.ok).toBe(false);
});
it('rejects second city-wide partner', () => {
const result = validatePartnerCityBinding(
[{ id: '1', partnerAccountId: '10', scopeType: 'CITY_WIDE' }],
{ partnerAccountId: '11', scopeType: 'CITY_WIDE' },
);
expect(result.ok).toBe(false);
});
it('rejects overlapping district codes', () => {
const result = validatePartnerCityBinding(
[{ id: '1', partnerAccountId: '10', scopeType: 'DISTRICT', districtCodes: ['410105'] }],
{ partnerAccountId: '11', scopeType: 'DISTRICT', districtCodes: ['410105'] },
);
expect(result.ok).toBe(false);
});
it('allows valid district binding', () => {
const result = validatePartnerCityBinding(
[{ id: '1', partnerAccountId: '10', scopeType: 'DISTRICT', districtCodes: ['410105'] }],
{ partnerAccountId: '11', scopeType: 'DISTRICT', districtCodes: ['410106'] },
);
expect(result.ok).toBe(true);
});
});
describe('validatePartnerCommissionRates', () => {
it('allows sum within default 5% cap', () => {
expect(validatePartnerCommissionRates(0.02, 0.03).ok).toBe(true);
});
it('rejects sum above cap', () => {
const result = validatePartnerCommissionRates(0.03, 0.03, 0.05);
expect(result.ok).toBe(false);
expect(result.message).toContain('5.00%');
});
it('respects custom city cap', () => {
expect(validatePartnerCommissionRates(0.04, 0.04, 0.08).ok).toBe(true);
expect(validatePartnerCommissionRates(0.05, 0.04, 0.08).ok).toBe(false);
});
});
+145
View File
@@ -0,0 +1,145 @@
export type CityPartnerScopeType = 'CITY_WIDE' | 'DISTRICT';
export type CityPartnerStatus = 'ACTIVE' | 'PAUSED';
export interface PartnerCityBindingInput {
id?: string;
partnerAccountId: string;
scopeType: CityPartnerScopeType;
districtCodes?: string[] | null;
bindingStatus?: CityPartnerStatus;
}
export interface PartnerCityResolveRef {
id: string;
partnerAccountId: string;
scopeType: CityPartnerScopeType;
orderCommissionRate: number;
redeemCommissionRate: number;
}
export interface PartnerCityValidationResult {
ok: boolean;
message?: string;
}
function normalizeDistrictCodes(codes?: string[] | null): string[] {
if (!codes?.length) return [];
return [...new Set(codes.map((c) => String(c).trim()).filter(Boolean))];
}
/** 区县 adcode 或名称命中区域合伙;否则全城合伙 */
export function resolveOrderCityPartner(
bindings: Array<{
id: string;
partnerAccountId: string;
scopeType: CityPartnerScopeType;
districtCodes?: string[] | null;
orderCommissionRate: number;
redeemCommissionRate: number;
bindingStatus?: CityPartnerStatus;
}>,
receiverDistrict?: string | null,
): PartnerCityResolveRef | null {
const active = bindings.filter((b) => b.bindingStatus !== 'PAUSED');
if (!active.length) return null;
const districtKey = receiverDistrict?.trim();
if (districtKey) {
const districtHit = active.find((b) => {
if (b.scopeType !== 'DISTRICT') return false;
const codes = normalizeDistrictCodes(b.districtCodes);
return codes.some((code) => code === districtKey || districtKey.includes(code) || code.includes(districtKey));
});
if (districtHit) {
return {
id: districtHit.id,
partnerAccountId: districtHit.partnerAccountId,
scopeType: districtHit.scopeType,
orderCommissionRate: districtHit.orderCommissionRate,
redeemCommissionRate: districtHit.redeemCommissionRate,
};
}
}
const cityWide = active.find((b) => b.scopeType === 'CITY_WIDE');
if (cityWide) {
return {
id: cityWide.id,
partnerAccountId: cityWide.partnerAccountId,
scopeType: cityWide.scopeType,
orderCommissionRate: cityWide.orderCommissionRate,
redeemCommissionRate: cityWide.redeemCommissionRate,
};
}
return null;
}
export function validatePartnerCityBinding(
existing: PartnerCityBindingInput[],
input: PartnerCityBindingInput,
excludeId?: string,
): PartnerCityValidationResult {
const others = existing.filter((b) => b.id !== excludeId);
if (others.some((b) => b.partnerAccountId === input.partnerAccountId)) {
return { ok: false, message: '该合伙人已绑定此城市' };
}
if (input.scopeType === 'CITY_WIDE') {
if (others.some((b) => b.scopeType === 'CITY_WIDE')) {
return { ok: false, message: '每城最多 1 名全城合伙人' };
}
return { ok: true };
}
const districts = normalizeDistrictCodes(input.districtCodes);
if (!districts.length) {
return { ok: false, message: '区域合伙人须至少选择一个区县' };
}
const occupied = new Set<string>();
for (const row of others) {
if (row.scopeType !== 'DISTRICT') continue;
for (const code of normalizeDistrictCodes(row.districtCodes)) {
occupied.add(code);
}
}
for (const code of districts) {
if (occupied.has(code)) {
return { ok: false, message: `区县 ${code} 已被其他区域合伙人占用` };
}
}
return { ok: true };
}
/** @deprecated use validatePartnerCityBinding */
export const validateCityPartnerBinding = validatePartnerCityBinding;
export const DEFAULT_MAX_PARTNER_COMMISSION_RATE = 0.05;
export function resolveMaxPartnerCommissionRate(maxRate?: number | null): number {
if (maxRate == null || Number.isNaN(Number(maxRate))) {
return DEFAULT_MAX_PARTNER_COMMISSION_RATE;
}
return Number(maxRate);
}
/** 订单佣金 + 核销佣金不得超过城市配置上限(默认 5%) */
export function validatePartnerCommissionRates(
orderCommissionRate: number,
redeemCommissionRate: number,
maxSumRate = DEFAULT_MAX_PARTNER_COMMISSION_RATE,
): PartnerCityValidationResult {
const sum = orderCommissionRate + redeemCommissionRate;
const max = resolveMaxPartnerCommissionRate(maxSumRate);
if (sum > max + 1e-9) {
return {
ok: false,
message: `订单佣金与核销佣金合计不得超过 ${(max * 100).toFixed(2)}%(当前 ${(sum * 100).toFixed(2)}%`,
};
}
return { ok: true };
}
+2
View File
@@ -125,3 +125,5 @@ export function orderTabToStatuses(tab: string): string[] | undefined {
return undefined;
}
}
export * from './city-partner';