feat(v3.4.16): partner UX, package pricing, CS gate, HQ list polish

Stop auto ST from validation_error; show package prices with right-aligned layout; partner store list status/filter and onboard CS QR gate; HQ store table truncation/fixed actions; expose CS config in wechat_mini settings; bind local servers for LAN.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 16:00:45 +08:00
parent 07630c9046
commit 46361ec713
21 changed files with 637 additions and 221 deletions
@@ -213,6 +213,23 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [
placeholder: '13203801799',
description: 'C 端联系客服拨号号码',
},
{
key: 'PARTNER_ONBOARD_CS_QR_URL',
label: '合伙人入驻 · 企微客服二维码',
group: G.wechat_mini,
type: 'image',
requiresRestart: false,
description: '合伙人 H5 录入门店提交前展示;未配置时禁止提交入驻',
},
{
key: 'PARTNER_ONBOARD_CS_HINT',
label: '合伙人入驻 · 客服提示文案',
group: G.wechat_mini,
type: 'string',
requiresRestart: false,
placeholder: '使用问题、提现问题等随时可联系【杜康好客】客服',
description: '二维码下方说明;留空用默认文案',
},
{
key: 'MOCK_SMS_FIXED_CODE',
label: 'Mock 短信固定验证码',
+4 -3
View File
@@ -86,12 +86,13 @@ async function bootstrap() {
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
app.useGlobalFilters(new HttpExceptionFilter(app.get(AlertService)));
app.useGlobalInterceptors(new ResponseInterceptor(), app.get(LoggingInterceptor));
const port = process.env.PORT || 3010;
const port = Number(process.env.PORT || 3010);
const host = process.env.HOST || '0.0.0.0';
const cfg = loadAppConfig();
const smsMode = cfg.mockSms ? 'MOCK' : 'ALIYUN';
console.log(`[config] NODE_ENV=${process.env.NODE_ENV} MOCK_SMS=${cfg.mockSms} SMS=${smsMode}`);
await app.listen(port);
console.log(`dukang-api listening on http://localhost:${port}/api/v1`);
await app.listen(port, host);
console.log(`dukang-api listening on http://${host}:${port}/api/v1`);
}
bootstrap();
@@ -37,6 +37,10 @@ export class ClientConfigController {
brandLogoMarkUrl: brand.brandLogoMarkUrl,
qualificationDisclosureUrl: brand.qualificationDisclosureUrl,
customerServicePhone: brand.customerServicePhone,
partnerOnboardCsQrUrl: (env.PARTNER_ONBOARD_CS_QR_URL ?? '').trim() || null,
partnerOnboardCsHint:
(env.PARTNER_ONBOARD_CS_HINT ?? '').trim() ||
'使用问题、提现问题等随时可联系【杜康好客】客服',
share,
};
}
@@ -5,7 +5,6 @@ import { AlertService } from '../../common/alert/alert.service';
import type { AlertLevel } from '../../common/alert/alert.constants';
import type { AuthUser } from '../../common/guards/jwt-auth.guard';
import type { ReportClientErrorDto } from './dto/client-error.dto';
import { SupportTicketService } from './support-ticket.service';
const WECOM_LEVELS = new Set(['fatal', 'error']);
@@ -16,7 +15,6 @@ export class ClientErrorService {
constructor(
private readonly prisma: PrismaService,
private readonly alert: AlertService,
private readonly supportTicket: SupportTicketService,
) {}
async report(dto: ReportClientErrorDto, user?: AuthUser, headerClientApp?: string) {
@@ -109,25 +107,6 @@ export class ClientErrorService {
);
}
if (dto.category === 'validation_error') {
const apiPath =
dto.extra && typeof dto.extra.url === 'string' ? dto.extra.url.slice(0, 256) : undefined;
void this.supportTicket
.createFromClientValidation({
clientApp,
message,
pagePath,
apiPath,
actorLabel:
user?.actorId != null ? `${user.actorType}:${String(user.actorId)}` : undefined,
})
.catch((e) => {
this.logger.warn(
`auto support ticket failed: ${e instanceof Error ? e.message : String(e)}`,
);
});
}
return { ok: true };
}
}
@@ -38,17 +38,8 @@ function generateSupportTicketNo() {
return `ST${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
}
const AUTO_VALIDATION_TICKET_APPS = new Set([
'USER_H5',
'USER_MINI',
'SHOP_H5',
'PARTNER_H5',
]);
@Injectable()
export class SupportTicketService {
private systemCreatorCache: { id: bigint; name: string } | null = null;
constructor(
private readonly prisma: PrismaService,
private readonly alert: AlertService,
@@ -105,66 +96,6 @@ export class SupportTicketService {
return serializeBigInt(mapSupportTicketRow(ticket));
}
/** 客户端 400 验证错误自动建单(1 小时内同标题去重) */
async createFromClientValidation(input: {
clientApp: string;
message: string;
pagePath?: string;
apiPath?: string;
actorLabel?: string;
}) {
if (!AUTO_VALIDATION_TICKET_APPS.has(input.clientApp)) {
return { skipped: true as const, reason: 'unsupported_app' as const };
}
const title = `[客户端验证] ${input.clientApp}${input.pagePath ? ` · ${input.pagePath}` : ''} · ${input.message.slice(0, 60)}`;
const oneHourAgo = new Date(Date.now() - 3600_000);
const existing = await this.prisma.commonSupportTicket.findFirst({
where: { title, createdAt: { gte: oneHourAgo } },
select: { id: true },
});
if (existing) {
return { skipped: true as const, ticketId: existing.id.toString() };
}
const creator = await this.resolveSystemCreator();
const content = [
`端:${input.clientApp}`,
input.pagePath ? `页面:${input.pagePath}` : null,
input.apiPath ? `接口:${input.apiPath}` : null,
input.actorLabel ? `用户:${input.actorLabel}` : null,
'',
input.message,
]
.filter(Boolean)
.join('\n');
const ticket = await this.create(
{
ticketType: 'BUG',
title,
content,
remark: '客户端验证错误自动上报',
},
creator,
);
return { skipped: false as const, ticketId: String(ticket.id) };
}
private async resolveSystemCreator() {
if (this.systemCreatorCache) return this.systemCreatorCache;
const account = await this.prisma.hqAccount.findFirst({
where: { adminRole: 'SUPER_ADMIN', status: 'ACTIVE' },
orderBy: { id: 'asc' },
select: { id: true },
});
if (!account) {
throw new BadRequestException('未找到系统管理员账号,无法自动创建工单');
}
this.systemCreatorCache = { id: account.id, name: '系统自动' };
return this.systemCreatorCache;
}
async update(id: bigint, dto: UpdateSupportTicketDto) {
const ticket = await this.getOrThrow(id);
if (ticket.status !== 'PENDING_REVIEW') {