|
|
@@ -24,25 +24,92 @@ export class BetaApplicationsService {
|
|
|
.replaceAll("'", ''');
|
|
|
}
|
|
|
|
|
|
+ private mapRole(value: string): string {
|
|
|
+ return {
|
|
|
+ stylist: '个人发型设计师',
|
|
|
+ 'salon-owner': '沙龙负责人 / 主理人',
|
|
|
+ staff: '沙龙员工',
|
|
|
+ other: '其他',
|
|
|
+ }[value] ?? value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private mapLanguage(value?: string): string {
|
|
|
+ if (!value) return '未填写';
|
|
|
+ return { zh: '中文', ja: '日文', en: '英文' }[value] ?? value;
|
|
|
+ }
|
|
|
+
|
|
|
+ private mapFeature(value: string): string {
|
|
|
+ return {
|
|
|
+ 'customer-try-on': '顾客预约前 AI 试发型',
|
|
|
+ profile: '美发师公开主页',
|
|
|
+ templates: '自己的发型模板',
|
|
|
+ follow: '顾客管理 / 跟进提醒',
|
|
|
+ }[value] ?? value;
|
|
|
+ }
|
|
|
+
|
|
|
private buildApplicationEmail(input: CreateBetaApplicationDto) {
|
|
|
const to = this.configService.getOrThrow<string>('betaApplicationEmail.to');
|
|
|
const from = this.configService.getOrThrow<string>('betaApplicationEmail.from');
|
|
|
const fields = [
|
|
|
- ['姓名', input.name],
|
|
|
- ['邮箱', input.email],
|
|
|
- ['沙龙', input.salonName],
|
|
|
- ['地区', input.area],
|
|
|
- ['身份', input.role],
|
|
|
- ['联系方式', input.contact],
|
|
|
- ['作品链接', input.portfolioUrl || '未填写'],
|
|
|
- ['月顾客数', input.monthlyCustomers || '未填写'],
|
|
|
- ['语言', input.language || '未填写'],
|
|
|
- ['关注功能', input.features?.join('、') || '未填写'],
|
|
|
- ['当前痛点', input.painPoint || '未填写'],
|
|
|
+ ['姓名', input.name.trim()],
|
|
|
+ ['邮箱', input.email.trim().toLowerCase()],
|
|
|
+ ['沙龙名 / 店铺名', input.salonName.trim()],
|
|
|
+ ['所在地区', input.area.trim()],
|
|
|
+ ['你的角色', this.mapRole(input.role)],
|
|
|
+ ['常用联系方式', input.contact.trim()],
|
|
|
+ ['作品或预约页面 URL', input.portfolioUrl?.trim() || '未填写'],
|
|
|
+ ['每月接待顾客数', input.monthlyCustomers || '未填写'],
|
|
|
+ ['联系语言', this.mapLanguage(input.language)],
|
|
|
+ [
|
|
|
+ '最想测试的功能',
|
|
|
+ input.features?.length
|
|
|
+ ? input.features.map((feature) => this.mapFeature(feature)).join('、')
|
|
|
+ : '未填写',
|
|
|
+ ],
|
|
|
+ ['现在最想解决的问题', input.painPoint?.trim() || '未填写'],
|
|
|
];
|
|
|
- const html = `<h2>Aitori Stylist 新的内测申请</h2><table cellpadding="8" cellspacing="0" border="1" style="border-collapse:collapse">${fields
|
|
|
- .map(([label, value]) => `<tr><th align="left">${label}</th><td>${this.escapeHtml(value)}</td></tr>`)
|
|
|
- .join('')}</table>`;
|
|
|
+ const rows = fields
|
|
|
+ .map(([label, value], index) => {
|
|
|
+ const safeValue = this.escapeHtml(value);
|
|
|
+ const renderedValue =
|
|
|
+ label === '邮箱'
|
|
|
+ ? `<a href="mailto:${safeValue}" style="color:#8056d6;text-decoration:none;">${safeValue}</a>`
|
|
|
+ : label === '作品或预约页面 URL' && value !== '未填写'
|
|
|
+ ? `<a href="${safeValue}" style="color:#8056d6;text-decoration:underline;word-break:break-all;">${safeValue}</a>`
|
|
|
+ : safeValue;
|
|
|
+
|
|
|
+ return `<tr>
|
|
|
+ <td style="width:180px;padding:16px 18px;border-top:${index === 0 ? '0' : '1px solid #eeeaf5'};background:#faf8fd;color:#6f6a7b;font-size:13px;font-weight:700;line-height:1.6;vertical-align:top;">${this.escapeHtml(label)}</td>
|
|
|
+ <td style="padding:16px 18px;border-top:${index === 0 ? '0' : '1px solid #eeeaf5'};color:#292735;font-size:15px;font-weight:600;line-height:1.7;vertical-align:top;word-break:break-word;">${renderedValue}</td>
|
|
|
+ </tr>`;
|
|
|
+ })
|
|
|
+ .join('');
|
|
|
+ const html = `<!doctype html>
|
|
|
+<html lang="zh-CN">
|
|
|
+ <body style="margin:0;padding:0;background:#f5f2f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei',Arial,sans-serif;">
|
|
|
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f2f7;">
|
|
|
+ <tr><td align="center" style="padding:32px 16px;">
|
|
|
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:680px;background:#ffffff;border-radius:16px;overflow:hidden;box-shadow:0 12px 36px rgba(63,45,86,0.10);">
|
|
|
+ <tr><td style="padding:30px 34px;background:#9c70ed;color:#ffffff;">
|
|
|
+ <div style="font-size:12px;font-weight:800;letter-spacing:2px;text-transform:uppercase;opacity:0.86;">AITORI STYLIST BETA</div>
|
|
|
+ <h1 style="margin:10px 0 0;font-size:25px;line-height:1.35;">收到新的内测申请</h1>
|
|
|
+ </td></tr>
|
|
|
+ <tr><td style="padding:30px 34px 34px;">
|
|
|
+ <p style="margin:0 0 22px;color:#6f6a7b;font-size:14px;line-height:1.8;">申请人 <strong style="color:#292735;">${this.escapeHtml(input.name.trim())}</strong> 提交了 Aitori Stylist 内测申请。直接回复此邮件即可联系申请人。</p>
|
|
|
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #eeeaf5;border-radius:12px;border-collapse:separate;border-spacing:0;overflow:hidden;">${rows}</table>
|
|
|
+ <p style="margin:24px 0 0;padding:16px 18px;border-radius:10px;background:#f6f1ff;color:#655b73;font-size:13px;line-height:1.7;">此邮件由 Aitori Stylist 内测申请页面自动发送,请勿转发包含申请人联系方式的内容。</p>
|
|
|
+ </td></tr>
|
|
|
+ </table>
|
|
|
+ <p style="margin:18px 0 0;color:#a29cab;font-size:12px;">Aitori Stylist · Beta Application</p>
|
|
|
+ </td></tr>
|
|
|
+ </table>
|
|
|
+ </body>
|
|
|
+</html>`;
|
|
|
+ const text = [
|
|
|
+ 'Aitori Stylist 新的内测申请',
|
|
|
+ '',
|
|
|
+ ...fields.map(([label, value]) => `${label}:${value}`),
|
|
|
+ ].join('\n');
|
|
|
|
|
|
return {
|
|
|
from,
|
|
|
@@ -50,6 +117,7 @@ export class BetaApplicationsService {
|
|
|
replyTo: input.email.trim().toLowerCase(),
|
|
|
subject: `Aitori Stylist 内测申请:${input.name.trim()}`,
|
|
|
html,
|
|
|
+ text,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -78,6 +146,7 @@ export class BetaApplicationsService {
|
|
|
reply_to: message.replyTo,
|
|
|
subject: message.subject,
|
|
|
html: message.html,
|
|
|
+ text: message.text,
|
|
|
}),
|
|
|
signal: AbortSignal.timeout(10000),
|
|
|
});
|