feat(mini-user): v3.5.10 同城配送提示可配置并支持换行

承运商 HTML 按收货市下发;textarea 回车在 C 端转成换行。含门店去掉分享按钮与小程序企微客服。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 14:29:48 +08:00
parent fed8ff3d3a
commit 797b20979d
29 changed files with 758 additions and 28 deletions
@@ -8,6 +8,7 @@ import {
import {
DEFAULT_XFX_LOGISTICS_PRICING,
isXfxProviderCode,
sanitizeDeliveryHintHtml,
type LogisticsPricingRuleDto,
type XiaofeixiaProviderConfig,
type XiaofeixiaProviderConfigPublic,
@@ -30,6 +31,7 @@ export type CreateFulfillmentProviderInput = {
bankAccountNo?: string | null;
settlementMethod?: LogisticsSettlementMethod | string;
pricingRules?: LogisticsPricingRuleDto | null;
deliveryHintHtml?: string | null;
};
export type UpdateFulfillmentProviderInput = Partial<CreateFulfillmentProviderInput>;
@@ -149,6 +151,7 @@ export class FulfillmentProviderService {
bankAccountNo: this.normOptional(input.bankAccountNo),
settlementMethod,
pricingRulesJson,
deliveryHintHtml: sanitizeDeliveryHintHtml(input.deliveryHintHtml),
},
});
return this.toDto(row);
@@ -195,11 +198,43 @@ export class FulfillmentProviderService {
? { settlementMethod: this.parseSettlementMethod(input.settlementMethod)! }
: {}),
...(pricingRulesJson !== undefined ? { pricingRulesJson } : {}),
...(input.deliveryHintHtml !== undefined
? { deliveryHintHtml: sanitizeDeliveryHintHtml(input.deliveryHintHtml) }
: {}),
},
});
return this.toDto(row);
}
async remove(id: bigint) {
const current = await this.prisma.fulfillmentProvider.findUnique({ where: { id } });
if (!current) throw new NotFoundException('仓配承运商不存在');
const [billCount, ledgerCount] = await Promise.all([
this.prisma.logisticsBill.count({ where: { fulfillmentProviderId: id } }),
this.prisma.logisticsPrepaidLedger.count({ where: { fulfillmentProviderId: id } }),
]);
if (billCount > 0) {
throw new BadRequestException(`该承运商已有 ${billCount} 笔物流对账单,不能删除`);
}
if (ledgerCount > 0) {
throw new BadRequestException('该承运商已有充值/扣款流水,不能删除');
}
await this.prisma.$transaction(async (tx) => {
await tx.cityWarehouse.updateMany({
where: { fulfillmentProviderId: id },
data: { fulfillmentProviderId: null, fulfillmentMode: 'MANUAL' },
});
await tx.orderDelivery.updateMany({
where: { fulfillmentProviderId: id },
data: { fulfillmentProviderId: null },
});
await tx.fulfillmentProvider.delete({ where: { id } });
});
return { ok: true, id: id.toString() };
}
/** 充值(结算模块可复用) */
async rechargePrepaid(providerId: bigint, amount: number, remark?: string) {
if (!(amount > 0)) throw new BadRequestException('充值金额须大于 0');
@@ -413,6 +448,7 @@ export class FulfillmentProviderService {
settlementMethod?: string;
pricingRulesJson?: string | null;
prepaidBalance?: Prisma.Decimal | number;
deliveryHintHtml?: string | null;
createdAt: Date;
updatedAt: Date;
}) {
@@ -434,8 +470,61 @@ export class FulfillmentProviderService {
settlementMethod: row.settlementMethod ?? 'PREPAID',
pricingRules: this.parsePricingRules(row.pricingRulesJson ?? null),
prepaidBalance: Number(row.prepaidBalance ?? 0),
deliveryHintHtml: sanitizeDeliveryHintHtml(row.deliveryHintHtml ?? null),
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
});
}
async listLocalDeliveries(filter?: { cityCode?: string; cityName?: string }) {
const cityCode = filter?.cityCode?.trim() || '';
const cityName = filter?.cityName?.trim() || '';
const cities = await this.prisma.commonCity.findMany({
where: {
status: 'ACTIVE',
...(cityCode ? { code: cityCode } : {}),
...(cityName && !cityCode
? { name: { in: this.cityNameAliases(cityName) } }
: {}),
},
include: {
warehouses: {
where: { status: 'ACTIVE' },
include: { fulfillmentProvider: true },
orderBy: { createdAt: 'asc' },
},
},
orderBy: { name: 'asc' },
});
return cities.map((city) => {
const preferred =
city.warehouses.find((w) => w.fulfillmentMode === 'API_AUTO' && w.fulfillmentProviderId) ??
city.warehouses.find((w) => w.fulfillmentProviderId) ??
null;
const provider = preferred?.fulfillmentProvider ?? null;
return {
city: { id: city.id.toString(), code: city.code, name: city.name },
warehouse: preferred
? {
id: preferred.id.toString(),
name: preferred.name,
fulfillmentMode: preferred.fulfillmentMode,
}
: null,
provider: provider
? { id: provider.id.toString(), code: provider.code, name: provider.name }
: null,
hintHtml: sanitizeDeliveryHintHtml(provider?.deliveryHintHtml ?? null),
};
});
}
private cityNameAliases(name: string): string[] {
const raw = name.trim();
if (!raw) return [];
const noSuffix = raw.replace(/市$/, '');
const withSuffix = raw.endsWith('市') ? raw : `${raw}`;
return Array.from(new Set([raw, noSuffix, withSuffix]));
}
}
@@ -3,9 +3,11 @@ import { IntegrationsModule } from '../../integrations/integrations.module';
import { TradeModule } from '../trade/trade.module';
import { FulfillmentProviderService } from './fulfillment-provider.service';
import { FulfillmentService } from './fulfillment.service';
import { LocalDeliveriesController } from './local-deliveries.controller';
@Module({
imports: [IntegrationsModule, forwardRef(() => TradeModule)],
controllers: [LocalDeliveriesController],
providers: [FulfillmentProviderService, FulfillmentService],
exports: [FulfillmentProviderService, FulfillmentService],
})
@@ -0,0 +1,12 @@
import { Controller, Get, Query } from '@nestjs/common';
import { FulfillmentProviderService } from './fulfillment-provider.service';
@Controller('catalog')
export class LocalDeliveriesController {
constructor(private readonly providers: FulfillmentProviderService) {}
@Get('local-deliveries')
list(@Query('cityCode') cityCode?: string, @Query('cityName') cityName?: string) {
return this.providers.listLocalDeliveries({ cityCode, cityName });
}
}