feat(mini-user): v3.5.10 同城配送提示可配置并支持换行
承运商 HTML 按收货市下发;textarea 回车在 C 端转成换行。含门店去掉分享按钮与小程序企微客服。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- v3.5.10:承运商「配送信息提示」(C 端同城送 HTML)
|
||||
ALTER TABLE `common_fulfillment_provider`
|
||||
ADD COLUMN `delivery_hint_html` TEXT NULL AFTER `prepaid_balance`;
|
||||
@@ -1057,6 +1057,8 @@ model FulfillmentProvider {
|
||||
settlementMethod LogisticsSettlementMethod @default(PREPAID) @map("settlement_method")
|
||||
pricingRulesJson String? @map("pricing_rules_json") @db.Text
|
||||
prepaidBalance Decimal @default(0) @map("prepaid_balance") @db.Decimal(12, 2)
|
||||
/// C 端同城配送提示(HTML,消毒后下发)
|
||||
deliveryHintHtml String? @map("delivery_hint_html") @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BRAND_LOGO_URL,
|
||||
BRAND_LOGO_WIDE_URL,
|
||||
CUSTOMER_SERVICE_PHONE,
|
||||
CUSTOMER_SERVICE_WECOM_URL,
|
||||
DEFAULT_SHARE_BENEFIT_TITLE,
|
||||
DEFAULT_SHARE_DESC,
|
||||
DEFAULT_SHARE_HINT,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
DEFAULT_SHARE_ORDER_TITLE,
|
||||
DEFAULT_SHARE_STORES_TITLE,
|
||||
DEFAULT_SHARE_TITLE,
|
||||
DEFAULT_SHOP_H5_URL,
|
||||
DEFAULT_USER_H5_URL,
|
||||
MINI_USER_STATIC_OSS_BASE,
|
||||
MOCK_SMS_FIXED_CODE,
|
||||
@@ -154,6 +156,15 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
placeholder: 'https://user.example.com/user',
|
||||
description: '推广码二维码 / 未配置时的默认落地页前缀(无末尾斜杠)',
|
||||
},
|
||||
{
|
||||
key: 'SHOP_H5_URL',
|
||||
label: '门店 H5 落地页',
|
||||
group: G.wechat_mini,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: 'https://shop.dukanghaoke.com',
|
||||
description: '用户核销码二维码链接前缀;扫码直达门店核销确认页(无末尾斜杠)',
|
||||
},
|
||||
{
|
||||
key: 'BRAND_LOGO_OSS_BASE',
|
||||
label: '品牌 Logo OSS 根路径',
|
||||
@@ -213,6 +224,24 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
|
||||
placeholder: '13203801799',
|
||||
description: 'C 端联系客服拨号号码',
|
||||
},
|
||||
{
|
||||
key: 'CUSTOMER_SERVICE_WECOM_URL',
|
||||
label: '企微微信客服链接',
|
||||
group: G.wechat_mini,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: 'https://work.weixin.qq.com/kfid/kfc…',
|
||||
description: 'C 端在线客服 kfid 链接;小程序需同时配置下方企业 ID',
|
||||
},
|
||||
{
|
||||
key: 'WECOM_CORP_ID',
|
||||
label: '企微企业 ID(CorpID)',
|
||||
group: G.wechat_mini,
|
||||
type: 'string',
|
||||
requiresRestart: false,
|
||||
placeholder: 'wwxxxxxxxxxxxx',
|
||||
description: '企业微信「我的企业」企业 ID。小程序须已关联该企业的微信客服;未填则回退小程序原生客服',
|
||||
},
|
||||
{
|
||||
key: 'PARTNER_ONBOARD_CS_QR_URL',
|
||||
label: '合伙人入驻 · 企微客服二维码',
|
||||
@@ -584,6 +613,7 @@ export const SYSTEM_CONFIG_KEY_SET = new Set(SYSTEM_CONFIG_FIELDS.map((f) => f.k
|
||||
/** 表单空值时展示 / 启动补种的默认值(与 shared-types 常量对齐) */
|
||||
export const SYSTEM_CONFIG_DEFAULTS: Record<string, string> = {
|
||||
USER_H5_URL: DEFAULT_USER_H5_URL.replace(/\/$/, ''),
|
||||
SHOP_H5_URL: DEFAULT_SHOP_H5_URL.replace(/\/$/, ''),
|
||||
BRAND_LOGO_OSS_BASE: BRAND_LOGO_OSS_BASE,
|
||||
BRAND_LOGO_URL: BRAND_LOGO_URL,
|
||||
BRAND_LOGO_WIDE_URL: BRAND_LOGO_WIDE_URL,
|
||||
@@ -591,6 +621,7 @@ export const SYSTEM_CONFIG_DEFAULTS: Record<string, string> = {
|
||||
MINI_USER_STATIC_OSS_BASE: MINI_USER_STATIC_OSS_BASE,
|
||||
QUALIFICATION_DISCLOSURE_URL: QUALIFICATION_DISCLOSURE_URL,
|
||||
CUSTOMER_SERVICE_PHONE: CUSTOMER_SERVICE_PHONE,
|
||||
CUSTOMER_SERVICE_WECOM_URL: CUSTOMER_SERVICE_WECOM_URL,
|
||||
MOCK_SMS_FIXED_CODE: MOCK_SMS_FIXED_CODE,
|
||||
SHARE_HINT: DEFAULT_SHARE_HINT,
|
||||
SHARE_DEFAULT_TITLE: DEFAULT_SHARE_TITLE,
|
||||
|
||||
@@ -37,6 +37,8 @@ export class ClientConfigController {
|
||||
brandLogoMarkUrl: brand.brandLogoMarkUrl,
|
||||
qualificationDisclosureUrl: brand.qualificationDisclosureUrl,
|
||||
customerServicePhone: brand.customerServicePhone,
|
||||
customerServiceWecomUrl: brand.customerServiceWecomUrl || null,
|
||||
wecomCorpId: brand.wecomCorpId || null,
|
||||
partnerOnboardCsQrUrl: (env.PARTNER_ONBOARD_CS_QR_URL ?? '').trim() || null,
|
||||
partnerOnboardCsHint:
|
||||
(env.PARTNER_ONBOARD_CS_HINT ?? '').trim() ||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||
@@ -47,6 +47,7 @@ export class AdminFulfillmentProvidersController {
|
||||
bankAccountNo: dto.bankAccountNo,
|
||||
settlementMethod: dto.settlementMethod,
|
||||
pricingRules: dto.pricingRules,
|
||||
deliveryHintHtml: dto.deliveryHintHtml,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +77,7 @@ export class AdminFulfillmentProvidersController {
|
||||
bankAccountNo: dto.bankAccountNo,
|
||||
settlementMethod: dto.settlementMethod,
|
||||
pricingRules: dto.pricingRules,
|
||||
deliveryHintHtml: dto.deliveryHintHtml,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,4 +91,14 @@ export class AdminFulfillmentProvidersController {
|
||||
recharge(@Param('id') id: string, @Body() dto: RechargeFulfillmentProviderDto) {
|
||||
return this.service.rechargePrepaid(BigInt(id), Number(dto.amount), dto.remark);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HqOperation({
|
||||
action: HqOperationAction.LOGISTICS_PROVIDER_DELETE,
|
||||
refType: 'FULFILLMENT_PROVIDER',
|
||||
refIdParam: 'id',
|
||||
})
|
||||
remove(@Param('id') id: string) {
|
||||
return this.service.remove(BigInt(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +797,11 @@ export class CreateFulfillmentProviderDto {
|
||||
boxBottles?: number;
|
||||
boxFee?: number;
|
||||
} | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
deliveryHintHtml?: string | null;
|
||||
}
|
||||
|
||||
export class UpdateFulfillmentProviderDto {
|
||||
@@ -859,6 +864,11 @@ export class UpdateFulfillmentProviderDto {
|
||||
boxBottles?: number;
|
||||
boxFee?: number;
|
||||
} | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
deliveryHintHtml?: string | null;
|
||||
}
|
||||
|
||||
export class RechargeFulfillmentProviderDto {
|
||||
|
||||
Reference in New Issue
Block a user