emailTemplate.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. const sendEmail = require('./Email')
  2. const {
  3. renderEmail,
  4. escapeNl2br,
  5. pEsc,
  6. kv,
  7. panel,
  8. notice,
  9. codeBox
  10. } = require('./emailLayout')
  11. class emailTemplate {
  12. stramptoTime(time) {
  13. if (time < 10)
  14. return ''
  15. return new Date(+time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
  16. }
  17. async checkEmail(email, code) {
  18. const bodyHtml = [
  19. pEsc('尊敬的用户:'),
  20. pEsc('您正在进行邮箱验证。如非本人操作,请忽略本邮件。'),
  21. pEsc('您的验证码为:'),
  22. codeBox(code),
  23. notice('验证码 5 分钟内有效,超时请重新获取。', 'danger')
  24. ].join('')
  25. await sendEmail(
  26. email,
  27. 'RunForge|邮箱验证码',
  28. renderEmail({
  29. pageTitle: '邮箱验证码',
  30. headline: '邮箱验证码',
  31. variant: 'default',
  32. bodyHtml,
  33. footerExtra: '请勿向他人泄露验证码。'
  34. })
  35. )
  36. }
  37. async bindEmailSuccess(email) {
  38. const time = this.stramptoTime(new Date().getTime())
  39. const bodyHtml = [
  40. pEsc('尊敬的用户:'),
  41. pEsc('您的 RunForge 账号绑定邮箱已成功更新。'),
  42. panel(kv('操作时间', time)),
  43. pEsc('如非本人操作,请尽快联系客服处理。', { last: true })
  44. ].join('')
  45. await sendEmail(
  46. email,
  47. 'RunForge|邮箱换绑成功',
  48. renderEmail({
  49. pageTitle: '邮箱换绑成功',
  50. headline: '邮箱换绑成功',
  51. variant: 'success',
  52. bodyHtml
  53. })
  54. )
  55. }
  56. async registerSuccess(email, username) {
  57. const time = new Date().getTime()
  58. const bodyHtml = [
  59. pEsc(`尊敬的 ${username}:`),
  60. pEsc('您已成功注册 RunForge 账号,账号信息如下:'),
  61. panel([
  62. kv('用户名', username),
  63. kv('注册时间', this.stramptoTime(time))
  64. ].join('')),
  65. pEsc('感谢您的使用。', { last: true })
  66. ].join('')
  67. await sendEmail(
  68. email,
  69. 'RunForge|注册成功',
  70. renderEmail({
  71. pageTitle: '注册成功',
  72. headline: '注册成功',
  73. variant: 'success',
  74. bodyHtml,
  75. footerExtra: '如非本人注册,请尽快联系客服处理。'
  76. })
  77. )
  78. }
  79. async updateSuccess(email, data) {
  80. const autoNote = data.auto_run === 0
  81. ? '当前未开启自动乐跑。如需跑步,请登录 RunForge 后手动发起。'
  82. : '已为您开启自动乐跑,系统将按计划代为完成跑步任务,请留意后续邮件提醒。'
  83. const bodyHtml = [
  84. pEsc(`尊敬的 ${data.name}:`),
  85. pEsc('您的乐跑账号登录信息已更新成功,详情如下:'),
  86. panel([
  87. kv('学号', data.account),
  88. kv('年级', data.grade_id),
  89. kv('学院', data.academy_name),
  90. kv('更新时间', this.stramptoTime(new Date().getTime()))
  91. ].join('')),
  92. notice(autoNote, 'neutral'),
  93. notice('请在当前登录乐跑账号的设备上使用「智慧体育」小程序;请勿在其他设备登录该小程序,以免登录状态失效并需重新绑定。', 'neutral'),
  94. pEsc('如需帮助,请联系 RunForge 客服。', { last: true })
  95. ].join('')
  96. await sendEmail(
  97. email,
  98. 'RunForge|乐跑账号更新成功',
  99. renderEmail({
  100. pageTitle: '乐跑账号更新成功',
  101. headline: '乐跑账号信息已更新',
  102. variant: 'success',
  103. bodyHtml
  104. })
  105. )
  106. }
  107. async lepaoOver(email, data) {
  108. const bodyHtml = [
  109. pEsc(`尊敬的 ${data.name}:`),
  110. pEsc('您设定的乐跑目标已全部完成,系统已为您关闭自动乐跑功能。'),
  111. pEsc('感谢您的配合。若仍需跑步,可在 RunForge 中按需重新开启相关功能。'),
  112. notice('如有疑问,请通过 RunForge 工单反馈。', 'neutral'),
  113. pEsc('祝您学习生活愉快。', { last: true })
  114. ].join('')
  115. await sendEmail(
  116. email,
  117. 'RunForge|乐跑目标已完成',
  118. renderEmail({
  119. pageTitle: '乐跑目标已完成',
  120. headline: '乐跑目标已完成',
  121. variant: 'success',
  122. bodyHtml
  123. })
  124. )
  125. }
  126. async lepaoSuccess(email, data) {
  127. const target_count = Number(data.target_count) || 0
  128. const total_num = Number(data.total_num) || 0
  129. const timeSec = Number(data.time) || 0
  130. const distanceKm = Number(data.distance) || 0
  131. const passTit = data.pass_tit != null && data.pass_tit !== '' ? data.pass_tit : '—'
  132. const paceStr =
  133. distanceKm > 0 && timeSec > 0
  134. ? this.calculatePace(timeSec, distanceKm)
  135. : '—'
  136. const timeStr = timeSec > 0 ? this.formatSecondsToMinSec(timeSec) : '—'
  137. let goalLines = ''
  138. if (target_count === 0) {
  139. goalLines = kv('累计次数', `${total_num} 次`)
  140. } else {
  141. goalLines = [
  142. kv('目标次数', `${target_count} 次`),
  143. kv('累计次数', `${total_num} 次`)
  144. ].join('')
  145. }
  146. const bodyHtml = [
  147. pEsc(`尊敬的 ${data.name}:`),
  148. pEsc('系统已成功代您完成一次乐跑,摘要如下:'),
  149. panel([
  150. kv('学号', data.account),
  151. kv('跑区', passTit),
  152. kv('用时', timeStr),
  153. kv('平均配速', paceStr),
  154. kv('距离', `${distanceKm || '—'} km`),
  155. goalLines
  156. ].join('')),
  157. notice('若已开启自动乐跑,请勿在除更新乐跑账号信息以外的其他设备登录「智慧体育」小程序,以免登录状态失效。', 'neutral')
  158. ].join('')
  159. await sendEmail(
  160. email,
  161. 'RunForge|乐跑成功通知',
  162. renderEmail({
  163. pageTitle: '乐跑成功',
  164. headline: '乐跑成功通知',
  165. variant: 'success',
  166. bodyHtml
  167. })
  168. )
  169. }
  170. async lepaoFail(email, data) {
  171. const time = new Date().getTime()
  172. const displayName = data.name ?? data.account
  173. const bodyHtml = [
  174. pEsc(`尊敬的 ${displayName}:`),
  175. pEsc('系统在尝试执行乐跑任务时未成功,详情如下:'),
  176. panel([
  177. kv('学号', data.account),
  178. kv('时间', this.stramptoTime(time)),
  179. kv('原因', data.reason)
  180. ].join('')),
  181. notice('若为登录失效,请在 RunForge 乐跑登录器中重新完成登录后再试。', 'warning'),
  182. pEsc('若问题持续存在,请联系 RunForge 客服并说明学号与失败时间,便于排查。', { last: true })
  183. ].join('')
  184. await sendEmail(
  185. email,
  186. 'RunForge|乐跑失败通知',
  187. renderEmail({
  188. pageTitle: '乐跑失败',
  189. headline: '乐跑失败通知',
  190. variant: 'danger',
  191. bodyHtml
  192. })
  193. )
  194. }
  195. async orderNewReply(email, data) {
  196. const files = Array.isArray(data.files) ? data.files : []
  197. const filesNote = files.length > 0
  198. ? notice('本回复包含附件,请登录 RunForge 官网,在工单详情中查看。', 'neutral')
  199. : ''
  200. const bodyHtml = [
  201. pEsc('尊敬的用户:'),
  202. pEsc(`您提交的工单(编号 ${data.id})有新的回复:`),
  203. panel([
  204. '<p><strong>回复内容:</strong></p>',
  205. `<p style="margin-top:8px;line-height:1.55;">${escapeNl2br(data.content)}</p>`,
  206. kv('回复时间', this.stramptoTime(new Date().getTime()))
  207. ].join('')),
  208. filesNote,
  209. notice('请登录 RunForge 官网,在工单详情中回复。', 'warning'),
  210. pEsc('感谢您的理解与配合。', { last: true })
  211. ].join('')
  212. await sendEmail(
  213. email,
  214. 'RunForge|工单新回复',
  215. renderEmail({
  216. pageTitle: '工单状态更新',
  217. headline: '工单有新回复',
  218. variant: 'default',
  219. bodyHtml
  220. })
  221. )
  222. }
  223. async sendCountRequestNotifyAdmins(email, data) {
  224. const bodyHtml = [
  225. pEsc('尊敬的管理员:'),
  226. pEsc('系统收到一条新的「赠送次数」申请,请及时登录管理后台审核处理。'),
  227. panel([
  228. kv('申请 ID', String(data.requestId)),
  229. kv('赠送人', data.senderUsername),
  230. kv('接收人', data.receiverUsername),
  231. kv('赠送次数', String(data.count)),
  232. kv('申请时间', this.stramptoTime(data.createTime))
  233. ].join('')),
  234. notice('请前往 RunForge 管理后台完成审核。', 'warning'),
  235. pEsc('谢谢。', { last: true })
  236. ].join('')
  237. await sendEmail(
  238. email,
  239. 'RunForge|赠送次数待审核',
  240. renderEmail({
  241. pageTitle: '赠送次数待审核',
  242. headline: '赠送次数待审核',
  243. variant: 'warning',
  244. bodyHtml
  245. })
  246. )
  247. }
  248. async lepaoAdminWarning(email, data) {
  249. const time = new Date().getTime()
  250. const safe = (v) => (v === undefined || v === null) ? '' : String(v)
  251. const subject = 'RunForge|乐跑异常告警'
  252. const bodyHtml = [
  253. pEsc('尊敬的管理员:'),
  254. pEsc('自动乐跑流程中出现非常见错误,请关注是否为接口变更、网络异常或程序缺陷,并及时跟进。'),
  255. panel([
  256. kv('账号', safe(data.account)),
  257. kv('姓名', safe(data.name)),
  258. kv('任务', safe(data.taskType)),
  259. kv('TraceId', safe(data.traceId)),
  260. kv('时间', this.stramptoTime(time)),
  261. kv('错误信息', safe(data.reason)),
  262. kv('错误码', safe(data.code))
  263. ].join('')),
  264. pEsc('以上为系统自动告警,如需更多信息请结合服务端日志进一步排查。', { last: true })
  265. ].join('')
  266. await sendEmail(
  267. email,
  268. subject,
  269. renderEmail({
  270. pageTitle: '乐跑异常告警',
  271. headline: '乐跑异常告警',
  272. headlineSuffixHtml: '<span class="tag">非常见错误</span>',
  273. variant: 'danger',
  274. bodyHtml
  275. })
  276. )
  277. }
  278. async sendCountRequestApproved(email, data) {
  279. const bodyHtml = [
  280. pEsc('尊敬的用户:'),
  281. pEsc('您收到的一笔赠送次数申请已通过审核,次数已到账:'),
  282. panel([
  283. kv('赠送人', data.senderUsername),
  284. kv('到账次数', String(data.count)),
  285. kv('审核时间', this.stramptoTime(data.reviewTime))
  286. ].join('')),
  287. pEsc('您可在 RunForge 中查看账户余额与明细。', { last: true })
  288. ].join('')
  289. await sendEmail(
  290. email,
  291. 'RunForge|赠送次数审核通过',
  292. renderEmail({
  293. pageTitle: '赠送次数审核通过',
  294. headline: '赠送次数已到账',
  295. variant: 'success',
  296. bodyHtml
  297. })
  298. )
  299. }
  300. async sendCountRequestRejected(email, data) {
  301. const bodyHtml = [
  302. pEsc('尊敬的用户:'),
  303. pEsc('您发起的一笔赠送次数申请未通过审核,相关次数已退回至您的账户:'),
  304. panel([
  305. kv('接收人', data.receiverUsername),
  306. kv('退回次数', String(data.count)),
  307. kv('审核时间', this.stramptoTime(data.reviewTime)),
  308. kv('拒绝原因', data.rejectReason)
  309. ].join('')),
  310. notice('如有疑问,请通过 RunForge 联系客服处理。', 'neutral'),
  311. pEsc('感谢您的理解。', { last: true })
  312. ].join('')
  313. await sendEmail(
  314. email,
  315. 'RunForge|赠送次数审核未通过',
  316. renderEmail({
  317. pageTitle: '赠送次数审核未通过',
  318. headline: '赠送次数审核未通过',
  319. variant: 'warning',
  320. bodyHtml
  321. })
  322. )
  323. }
  324. async powerCheck(email, data) {
  325. const gap = Number(data.lowest) - Number(data.now_balance)
  326. const gapStr = Number.isFinite(gap) ? gap.toFixed(2) : ''
  327. const bodyHtml = [
  328. pEsc('尊敬的用户:'),
  329. pEsc(`${data.building}${data.room} 宿舍电费已低于您设定的提醒阈值,请关注余额并及时充值。`),
  330. panel([
  331. kv('校区', data.area),
  332. kv('楼栋', data.building),
  333. kv('寝室号', data.room),
  334. kv('当前余额', `¥${data.now_balance}`),
  335. kv('提醒阈值', `¥${data.lowest}`),
  336. kv('最近扣费时间', data.now_change_time)
  337. ].join('')),
  338. notice(
  339. gapStr !== ''
  340. ? `当前余额已低于提醒阈值 ${gapStr} 元,建议尽快充值以免影响用电。`
  341. : '当前余额已低于提醒阈值,建议尽快充值以免影响用电。',
  342. 'warning'
  343. ),
  344. pEsc('您可在 RunForge 中调整提醒阈值或管理电费订阅。', { last: true })
  345. ].join('')
  346. await sendEmail(
  347. email,
  348. 'RunForge|宿舍电费提醒',
  349. renderEmail({
  350. pageTitle: '宿舍电费提醒',
  351. headline: '宿舍电费余额提醒',
  352. variant: 'warning',
  353. bodyHtml
  354. })
  355. )
  356. }
  357. formatSecondsToMinSec(totalSeconds) {
  358. const minutes = Math.floor(totalSeconds / 60)
  359. const seconds = totalSeconds % 60
  360. return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
  361. }
  362. calculatePace(seconds, kilometers) {
  363. const paceInSeconds = seconds / kilometers
  364. const minutes = Math.floor(paceInSeconds / 60)
  365. const remainingSeconds = Math.round(paceInSeconds % 60)
  366. return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`
  367. }
  368. }
  369. const EmailTemplate = new emailTemplate()
  370. module.exports = EmailTemplate