const sendEmail = require('./Email') const { renderEmail, escapeNl2br, pEsc, kv, panel, notice, codeBox } = require('./emailLayout') class emailTemplate { stramptoTime(time) { if (time < 10) return '' return new Date(+time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } async checkEmail(email, code) { const bodyHtml = [ pEsc('尊敬的用户:'), pEsc('您正在进行邮箱验证。如非本人操作,请忽略本邮件。'), pEsc('您的验证码为:'), codeBox(code), notice('验证码 5 分钟内有效,超时请重新获取。', 'danger') ].join('') await sendEmail( email, 'RunForge|邮箱验证码', renderEmail({ pageTitle: '邮箱验证码', headline: '邮箱验证码', variant: 'default', bodyHtml, footerExtra: '请勿向他人泄露验证码。' }) ) } async bindEmailSuccess(email) { const time = this.stramptoTime(new Date().getTime()) const bodyHtml = [ pEsc('尊敬的用户:'), pEsc('您的 RunForge 账号绑定邮箱已成功更新。'), panel(kv('操作时间', time)), pEsc('如非本人操作,请尽快联系客服处理。', { last: true }) ].join('') await sendEmail( email, 'RunForge|邮箱换绑成功', renderEmail({ pageTitle: '邮箱换绑成功', headline: '邮箱换绑成功', variant: 'success', bodyHtml }) ) } async registerSuccess(email, username) { const time = new Date().getTime() const bodyHtml = [ pEsc(`尊敬的 ${username}:`), pEsc('您已成功注册 RunForge 账号,账号信息如下:'), panel([ kv('用户名', username), kv('注册时间', this.stramptoTime(time)) ].join('')), pEsc('感谢您的使用。', { last: true }) ].join('') await sendEmail( email, 'RunForge|注册成功', renderEmail({ pageTitle: '注册成功', headline: '注册成功', variant: 'success', bodyHtml, footerExtra: '如非本人注册,请尽快联系客服处理。' }) ) } async updateSuccess(email, data) { const autoNote = data.auto_run === 0 ? '当前未开启自动乐跑。如需跑步,请登录 RunForge 后手动发起。' : '已为您开启自动乐跑,系统将按计划代为完成跑步任务,请留意后续邮件提醒。' const bodyHtml = [ pEsc(`尊敬的 ${data.name}:`), pEsc('您的乐跑账号登录信息已更新成功,详情如下:'), panel([ kv('学号', data.account), kv('年级', data.grade_id), kv('学院', data.academy_name), kv('更新时间', this.stramptoTime(new Date().getTime())) ].join('')), notice(autoNote, 'neutral'), notice('请在当前登录乐跑账号的设备上使用「智慧体育」小程序;请勿在其他设备登录该小程序,以免登录状态失效并需重新绑定。', 'neutral'), pEsc('如需帮助,请联系 RunForge 客服。', { last: true }) ].join('') await sendEmail( email, 'RunForge|乐跑账号更新成功', renderEmail({ pageTitle: '乐跑账号更新成功', headline: '乐跑账号信息已更新', variant: 'success', bodyHtml }) ) } async lepaoOver(email, data) { const bodyHtml = [ pEsc(`尊敬的 ${data.name}:`), pEsc('您设定的乐跑目标已全部完成,系统已为您关闭自动乐跑功能。'), pEsc('感谢您的配合。若仍需跑步,可在 RunForge 中按需重新开启相关功能。'), notice('如有疑问,请通过 RunForge 工单反馈。', 'neutral'), pEsc('祝您学习生活愉快。', { last: true }) ].join('') await sendEmail( email, 'RunForge|乐跑目标已完成', renderEmail({ pageTitle: '乐跑目标已完成', headline: '乐跑目标已完成', variant: 'success', bodyHtml }) ) } async lepaoSuccess(email, data) { const target_count = Number(data.target_count) || 0 const total_num = Number(data.total_num) || 0 const timeSec = Number(data.time) || 0 const distanceKm = Number(data.distance) || 0 const passTit = data.pass_tit != null && data.pass_tit !== '' ? data.pass_tit : '—' const paceStr = distanceKm > 0 && timeSec > 0 ? this.calculatePace(timeSec, distanceKm) : '—' const timeStr = timeSec > 0 ? this.formatSecondsToMinSec(timeSec) : '—' let goalLines = '' if (target_count === 0) { goalLines = kv('累计次数', `${total_num} 次`) } else { goalLines = [ kv('目标次数', `${target_count} 次`), kv('累计次数', `${total_num} 次`) ].join('') } const bodyHtml = [ pEsc(`尊敬的 ${data.name}:`), pEsc('系统已成功代您完成一次乐跑,摘要如下:'), panel([ kv('学号', data.account), kv('跑区', passTit), kv('用时', timeStr), kv('平均配速', paceStr), kv('距离', `${distanceKm || '—'} km`), goalLines ].join('')), notice('若已开启自动乐跑,请勿在除更新乐跑账号信息以外的其他设备登录「智慧体育」小程序,以免登录状态失效。', 'neutral') ].join('') await sendEmail( email, 'RunForge|乐跑成功通知', renderEmail({ pageTitle: '乐跑成功', headline: '乐跑成功通知', variant: 'success', bodyHtml }) ) } async lepaoFail(email, data) { const time = new Date().getTime() const displayName = data.name ?? data.account const bodyHtml = [ pEsc(`尊敬的 ${displayName}:`), pEsc('系统在尝试执行乐跑任务时未成功,详情如下:'), panel([ kv('学号', data.account), kv('时间', this.stramptoTime(time)), kv('原因', data.reason) ].join('')), notice('若为登录失效,请在 RunForge 乐跑登录器中重新完成登录后再试。', 'warning'), pEsc('若问题持续存在,请联系 RunForge 客服并说明学号与失败时间,便于排查。', { last: true }) ].join('') await sendEmail( email, 'RunForge|乐跑失败通知', renderEmail({ pageTitle: '乐跑失败', headline: '乐跑失败通知', variant: 'danger', bodyHtml }) ) } async orderNewReply(email, data) { const files = Array.isArray(data.files) ? data.files : [] const filesNote = files.length > 0 ? notice('本回复包含附件,请登录 RunForge 官网,在工单详情中查看。', 'neutral') : '' const bodyHtml = [ pEsc('尊敬的用户:'), pEsc(`您提交的工单(编号 ${data.id})有新的回复:`), panel([ '
回复内容:
', `${escapeNl2br(data.content)}
`, kv('回复时间', this.stramptoTime(new Date().getTime())) ].join('')), filesNote, notice('请登录 RunForge 官网,在工单详情中回复。', 'warning'), pEsc('感谢您的理解与配合。', { last: true }) ].join('') await sendEmail( email, 'RunForge|工单新回复', renderEmail({ pageTitle: '工单状态更新', headline: '工单有新回复', variant: 'default', bodyHtml }) ) } async sendCountRequestNotifyAdmins(email, data) { const bodyHtml = [ pEsc('尊敬的管理员:'), pEsc('系统收到一条新的「赠送次数」申请,请及时登录管理后台审核处理。'), panel([ kv('申请 ID', String(data.requestId)), kv('赠送人', data.senderUsername), kv('接收人', data.receiverUsername), kv('赠送次数', String(data.count)), kv('申请时间', this.stramptoTime(data.createTime)) ].join('')), notice('请前往 RunForge 管理后台完成审核。', 'warning'), pEsc('谢谢。', { last: true }) ].join('') await sendEmail( email, 'RunForge|赠送次数待审核', renderEmail({ pageTitle: '赠送次数待审核', headline: '赠送次数待审核', variant: 'warning', bodyHtml }) ) } async lepaoAdminWarning(email, data) { const time = new Date().getTime() const safe = (v) => (v === undefined || v === null) ? '' : String(v) const subject = 'RunForge|乐跑异常告警' const bodyHtml = [ pEsc('尊敬的管理员:'), pEsc('自动乐跑流程中出现非常见错误,请关注是否为接口变更、网络异常或程序缺陷,并及时跟进。'), panel([ kv('账号', safe(data.account)), kv('姓名', safe(data.name)), kv('任务', safe(data.taskType)), kv('TraceId', safe(data.traceId)), kv('时间', this.stramptoTime(time)), kv('错误信息', safe(data.reason)), kv('错误码', safe(data.code)) ].join('')), pEsc('以上为系统自动告警,如需更多信息请结合服务端日志进一步排查。', { last: true }) ].join('') await sendEmail( email, subject, renderEmail({ pageTitle: '乐跑异常告警', headline: '乐跑异常告警', headlineSuffixHtml: '非常见错误', variant: 'danger', bodyHtml }) ) } async sendCountRequestApproved(email, data) { const bodyHtml = [ pEsc('尊敬的用户:'), pEsc('您收到的一笔赠送次数申请已通过审核,次数已到账:'), panel([ kv('赠送人', data.senderUsername), kv('到账次数', String(data.count)), kv('审核时间', this.stramptoTime(data.reviewTime)) ].join('')), pEsc('您可在 RunForge 中查看账户余额与明细。', { last: true }) ].join('') await sendEmail( email, 'RunForge|赠送次数审核通过', renderEmail({ pageTitle: '赠送次数审核通过', headline: '赠送次数已到账', variant: 'success', bodyHtml }) ) } async sendCountRequestRejected(email, data) { const bodyHtml = [ pEsc('尊敬的用户:'), pEsc('您发起的一笔赠送次数申请未通过审核,相关次数已退回至您的账户:'), panel([ kv('接收人', data.receiverUsername), kv('退回次数', String(data.count)), kv('审核时间', this.stramptoTime(data.reviewTime)), kv('拒绝原因', data.rejectReason) ].join('')), notice('如有疑问,请通过 RunForge 联系客服处理。', 'neutral'), pEsc('感谢您的理解。', { last: true }) ].join('') await sendEmail( email, 'RunForge|赠送次数审核未通过', renderEmail({ pageTitle: '赠送次数审核未通过', headline: '赠送次数审核未通过', variant: 'warning', bodyHtml }) ) } async powerCheck(email, data) { const gap = Number(data.lowest) - Number(data.now_balance) const gapStr = Number.isFinite(gap) ? gap.toFixed(2) : '' const bodyHtml = [ pEsc('尊敬的用户:'), pEsc(`${data.building}${data.room} 宿舍电费已低于您设定的提醒阈值,请关注余额并及时充值。`), panel([ kv('校区', data.area), kv('楼栋', data.building), kv('寝室号', data.room), kv('当前余额', `¥${data.now_balance}`), kv('提醒阈值', `¥${data.lowest}`), kv('最近扣费时间', data.now_change_time) ].join('')), notice( gapStr !== '' ? `当前余额已低于提醒阈值 ${gapStr} 元,建议尽快充值以免影响用电。` : '当前余额已低于提醒阈值,建议尽快充值以免影响用电。', 'warning' ), pEsc('您可在 RunForge 中调整提醒阈值或管理电费订阅。', { last: true }) ].join('') await sendEmail( email, 'RunForge|宿舍电费提醒', renderEmail({ pageTitle: '宿舍电费提醒', headline: '宿舍电费余额提醒', variant: 'warning', bodyHtml }) ) } formatSecondsToMinSec(totalSeconds) { const minutes = Math.floor(totalSeconds / 60) const seconds = totalSeconds % 60 return `${minutes}分${seconds.toString().padStart(2, '0')}秒` } calculatePace(seconds, kilometers) { const paceInSeconds = seconds / kilometers const minutes = Math.floor(paceInSeconds / 60) const remainingSeconds = Math.round(paceInSeconds % 60) return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''` } } const EmailTemplate = new emailTemplate() module.exports = EmailTemplate