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:
@@ -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 短信固定验证码',
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user