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

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';
+2
View File
@@ -4,6 +4,8 @@ export interface CityDto {
name: string;
province: string;
status: string;
/** 订单+核销佣金合计上限(小数,默认 0.05) */
maxPartnerCommissionRate?: number;
}
export interface ProductDto {
+91
View File
@@ -0,0 +1,91 @@
import type { CityPartnerScopeType, CityPartnerStatus } from './enums';
/** @deprecated use PartnerPrimaryAccountDto */
export type CityPartnerDto = PartnerPrimaryAccountDto;
export interface PartnerPrimaryAccountDto {
id: string;
cityId: string;
cityName?: string | null;
cityCode?: string | null;
companyName: string;
contactPhone?: string | null;
address?: string | null;
phone: string;
name: string;
scopeType: CityPartnerScopeType;
districtCodes: string[] | null;
orderCommissionRate: number;
redeemCommissionRate: number;
bindingStatus: CityPartnerStatus;
managedWarehouseId?: string | null;
status: string;
createdAt: string;
updatedAt: string;
}
export interface CreatePartnerPrimaryInput {
cityId: string;
phone: string;
name: string;
companyName: string;
address: string;
contactPhone?: string;
scopeType: CityPartnerScopeType;
districtCodes?: string[];
orderCommissionRate?: number;
redeemCommissionRate?: number;
bindingStatus?: CityPartnerStatus;
managedWarehouseId?: string;
contractNo?: string;
bankAccountName?: string;
bankAccountNo?: string;
bankBranch?: string;
weeklyStoreTarget?: number;
}
export interface UpdatePartnerPrimaryInput {
name?: string;
phone?: string;
companyName?: string;
address?: string;
contactPhone?: string;
scopeType?: CityPartnerScopeType;
districtCodes?: string[];
orderCommissionRate?: number;
redeemCommissionRate?: number;
bindingStatus?: CityPartnerStatus;
managedWarehouseId?: string | null;
contractNo?: string;
bankAccountName?: string;
bankAccountNo?: string;
bankBranch?: string;
weeklyStoreTarget?: number;
status?: string;
}
export interface PartnerCityBindingRef {
id: string;
partnerAccountId: string;
scopeType: CityPartnerScopeType;
districtCodes: string[] | null;
orderCommissionRate: number;
redeemCommissionRate: number;
bindingStatus: CityPartnerStatus;
}
export const PARTNER_PERMISSION_KEYS = [
'warehouse:manage',
'store:manage',
'store:create',
'order:view',
] as const;
export type PartnerPermissionKey = (typeof PARTNER_PERMISSION_KEYS)[number];
export const PARTNER_PERMISSION_LABELS: Record<PartnerPermissionKey, string> = {
'warehouse:manage': '仓库管理',
'store:manage': '门店管理',
'store:create': '开店管理',
'order:view': '订单查看',
};
@@ -0,0 +1,36 @@
import type { WarehouseManagerType, WarehouseStatus } from './enums';
export interface CityWarehouseDto {
id: string;
cityId: string;
name: string;
address: string;
contactName: string;
contactPhone: string;
managerType: WarehouseManagerType;
partnerAccountId: string | null;
partnerCompanyName?: string | null;
status: WarehouseStatus;
createdAt: string;
updatedAt: string;
}
export interface CreateCityWarehouseInput {
name: string;
address: string;
contactName: string;
contactPhone: string;
managerType: WarehouseManagerType;
partnerAccountId?: string;
status?: WarehouseStatus;
}
export interface UpdateCityWarehouseInput {
name?: string;
address?: string;
contactName?: string;
contactPhone?: string;
managerType?: WarehouseManagerType;
partnerAccountId?: string | null;
status?: WarehouseStatus;
}
+40
View File
@@ -72,6 +72,46 @@ export enum PartnerStaffRole {
PROMOTER = 'PROMOTER',
}
export enum CityPartnerScopeType {
CITY_WIDE = 'CITY_WIDE',
DISTRICT = 'DISTRICT',
}
export enum CityPartnerStatus {
ACTIVE = 'ACTIVE',
PAUSED = 'PAUSED',
}
export enum WarehouseManagerType {
HQ = 'HQ',
PARTNER = 'PARTNER',
}
export enum WarehouseStatus {
ACTIVE = 'ACTIVE',
PAUSED = 'PAUSED',
}
export const CITY_PARTNER_SCOPE_LABELS: Record<CityPartnerScopeType, string> = {
[CityPartnerScopeType.CITY_WIDE]: '全城合伙人',
[CityPartnerScopeType.DISTRICT]: '区域合伙人',
};
export const WAREHOUSE_MANAGER_LABELS: Record<WarehouseManagerType, string> = {
[WarehouseManagerType.HQ]: '总部直管',
[WarehouseManagerType.PARTNER]: '合伙人管仓',
};
export const CITY_PARTNER_STATUS_LABELS: Record<CityPartnerStatus, string> = {
[CityPartnerStatus.ACTIVE]: '启用',
[CityPartnerStatus.PAUSED]: '暂停',
};
export const WAREHOUSE_STATUS_LABELS: Record<WarehouseStatus, string> = {
[WarehouseStatus.ACTIVE]: '启用',
[WarehouseStatus.PAUSED]: '暂停',
};
export enum AccountStatus {
ACTIVE = 'ACTIVE',
DISABLED = 'DISABLED',
+2
View File
@@ -15,3 +15,5 @@ export * from './partner-log';
export * from './promo';
export * from './hq-permissions';
export * from './partner';
export * from './city-partner';
export * from './city-warehouse';
+14 -1
View File
@@ -1,34 +1,47 @@
export type PartnerLogCategory =
| 'login'
| 'wechat_auth'
| 'account_ops'
| 'store_ops'
| 'shipping'
| 'settlement';
| 'settlement'
| 'warehouse_ops';
export const PARTNER_LOG_EVENT_CATEGORIES: Record<PartnerLogCategory, readonly string[]> = {
login: ['partner_sms_send', 'partner_sms_login', 'partner_sms_verify_fail', 'partner_login_success'],
wechat_auth: ['partner_wechat_login', 'partner_wechat_bind'],
account_ops: [
'partner_staff_create',
'partner_staff_update',
'partner_staff_delete',
'partner_staff_permission_update',
],
store_ops: ['partner_store_create', 'partner_store_status_change'],
shipping: ['partner_order_ship', 'partner_delivery_advance'],
settlement: ['partner_bill_view', 'partner_bill_detail_view'],
warehouse_ops: ['partner_warehouse_view', 'partner_warehouse_update'],
};
export const PARTNER_LOG_CATEGORY_OPTIONS: Array<{ value: PartnerLogCategory | ''; label: string }> = [
{ value: '', label: '全部' },
{ value: 'login', label: '登录' },
{ value: 'wechat_auth', label: '微信授权' },
{ value: 'account_ops', label: '子账号管理' },
{ value: 'store_ops', label: '门店操作' },
{ value: 'shipping', label: '发货/配送' },
{ value: 'settlement', label: '结算' },
{ value: 'warehouse_ops', label: '仓库操作' },
];
export const PARTNER_LOG_CATEGORY_LABELS: Record<PartnerLogCategory | '', string> = {
'': '全部',
login: '登录',
wechat_auth: '微信授权',
account_ops: '子账号管理',
store_ops: '门店操作',
shipping: '发货/配送',
settlement: '结算',
warehouse_ops: '仓库操作',
};
export function resolvePartnerLogCategory(eventName: string): PartnerLogCategory | null {
+5
View File
@@ -8,6 +8,8 @@ export interface PartnerMe {
companyName?: string;
hasWechat?: boolean;
staffRole?: PartnerStaffRole;
permissions?: string[];
primaryAccountId?: string;
}
export interface PartnerStaffItem {
@@ -15,6 +17,7 @@ export interface PartnerStaffItem {
name: string;
phone: string;
staffRole: PartnerStaffRole;
permissions?: string[];
status: AccountStatus;
lastLoginAt?: string;
}
@@ -22,11 +25,13 @@ export interface PartnerStaffItem {
export interface CreatePartnerStaffRequest {
phone: string;
name: string;
permissions?: string[];
}
export interface UpdatePartnerStaffRequest {
name?: string;
staffRole?: PartnerStaffRole;
permissions?: string[];
status?: AccountStatus;
}