emailTemplate.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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. pEsc('如需协助,请联系 RunForge 客服。', { last: true })
  159. ].join('')
  160. await sendEmail(
  161. email,
  162. 'RunForge|乐跑成功通知',
  163. renderEmail({
  164. pageTitle: '乐跑成功',
  165. headline: '乐跑已成功记录',
  166. variant: 'success',
  167. bodyHtml
  168. })
  169. )
  170. }
  171. async lepaoFail(email, data) {
  172. const time = new Date().getTime()
  173. const displayName = data.name ?? data.account
  174. const bodyHtml = [
  175. pEsc(`尊敬的 ${displayName}:`),
  176. pEsc('系统在尝试执行乐跑任务时未成功,详情如下:'),
  177. panel([
  178. kv('学号', data.account),
  179. kv('时间', this.stramptoTime(time)),
  180. kv('原因', data.reason)
  181. ].join('')),
  182. notice('若为登录失效,请在 RunForge 乐跑登录器中重新完成登录后再试。', 'warning'),
  183. pEsc('若问题持续存在,请联系 RunForge 客服并说明学号与失败时间,便于排查。', { last: true })
  184. ].join('')
  185. await sendEmail(
  186. email,
  187. 'RunForge|乐跑失败通知',
  188. renderEmail({
  189. pageTitle: '乐跑失败',
  190. headline: '乐跑未能完成',
  191. variant: 'danger',
  192. bodyHtml
  193. })
  194. )
  195. }
  196. async orderNewReply(email, data) {
  197. const files = Array.isArray(data.files) ? data.files : []
  198. const filesNote = files.length > 0
  199. ? notice('本回复包含附件,请登录 RunForge 官网,在工单详情中查看。', 'neutral')
  200. : ''
  201. const bodyHtml = [
  202. pEsc('尊敬的用户:'),
  203. pEsc(`您提交的工单(编号 ${data.id})有新的回复:`),
  204. panel([
  205. '<p><strong>回复内容:</strong></p>',
  206. `<p style="margin-top:8px;line-height:1.55;">${escapeNl2br(data.content)}</p>`,
  207. kv('回复时间', this.stramptoTime(new Date().getTime()))
  208. ].join('')),
  209. filesNote,
  210. notice('请登录 RunForge 官网,在工单详情中回复。', 'warning'),
  211. pEsc('感谢您的理解与配合。', { last: true })
  212. ].join('')
  213. await sendEmail(
  214. email,
  215. 'RunForge|工单新回复',
  216. renderEmail({
  217. pageTitle: '工单状态更新',
  218. headline: '工单有新回复',
  219. variant: 'default',
  220. bodyHtml
  221. })
  222. )
  223. }
  224. async sendCountRequestNotifyAdmins(email, data) {
  225. const bodyHtml = [
  226. pEsc('尊敬的管理员:'),
  227. pEsc('系统收到一条新的「赠送次数」申请,请及时登录管理后台审核处理。'),
  228. panel([
  229. kv('申请 ID', String(data.requestId)),
  230. kv('赠送人', data.senderUsername),
  231. kv('接收人', data.receiverUsername),
  232. kv('赠送次数', String(data.count)),
  233. kv('申请时间', this.stramptoTime(data.createTime))
  234. ].join('')),
  235. notice('请前往 RunForge 管理后台完成审核。', 'warning'),
  236. pEsc('谢谢。', { last: true })
  237. ].join('')
  238. await sendEmail(
  239. email,
  240. 'RunForge|赠送次数待审核',
  241. renderEmail({
  242. pageTitle: '赠送次数待审核',
  243. headline: '赠送次数待审核',
  244. variant: 'warning',
  245. bodyHtml
  246. })
  247. )
  248. }
  249. async lepaoAdminWarning(email, data) {
  250. const time = new Date().getTime()
  251. const safe = (v) => (v === undefined || v === null) ? '' : String(v)
  252. const subject = 'RunForge|乐跑异常告警'
  253. const bodyHtml = [
  254. pEsc('尊敬的管理员:'),
  255. pEsc('自动乐跑流程中出现非常见错误,请关注是否为接口变更、网络异常或程序缺陷,并及时跟进。'),
  256. panel([
  257. kv('账号', safe(data.account)),
  258. kv('姓名', safe(data.name)),
  259. kv('任务', safe(data.taskType)),
  260. kv('TraceId', safe(data.traceId)),
  261. kv('时间', this.stramptoTime(time)),
  262. kv('错误信息', safe(data.reason)),
  263. kv('错误码', safe(data.code))
  264. ].join('')),
  265. pEsc('以上为系统自动告警,如需更多信息请结合服务端日志进一步排查。', { last: true })
  266. ].join('')
  267. await sendEmail(
  268. email,
  269. subject,
  270. renderEmail({
  271. pageTitle: '乐跑异常告警',
  272. headline: '乐跑异常告警',
  273. headlineSuffixHtml: '<span class="tag">非常见错误</span>',
  274. variant: 'danger',
  275. bodyHtml
  276. })
  277. )
  278. }
  279. async sendCountRequestApproved(email, data) {
  280. const bodyHtml = [
  281. pEsc('尊敬的用户:'),
  282. pEsc('您收到的一笔赠送次数申请已通过审核,次数已到账:'),
  283. panel([
  284. kv('赠送人', data.senderUsername),
  285. kv('到账次数', String(data.count)),
  286. kv('审核时间', this.stramptoTime(data.reviewTime))
  287. ].join('')),
  288. pEsc('您可在 RunForge 中查看账户余额与明细。', { last: true })
  289. ].join('')
  290. await sendEmail(
  291. email,
  292. 'RunForge|赠送次数审核通过',
  293. renderEmail({
  294. pageTitle: '赠送次数审核通过',
  295. headline: '赠送次数已到账',
  296. variant: 'success',
  297. bodyHtml
  298. })
  299. )
  300. }
  301. async sendCountRequestRejected(email, data) {
  302. const bodyHtml = [
  303. pEsc('尊敬的用户:'),
  304. pEsc('您发起的一笔赠送次数申请未通过审核,相关次数已退回至您的账户:'),
  305. panel([
  306. kv('接收人', data.receiverUsername),
  307. kv('退回次数', String(data.count)),
  308. kv('审核时间', this.stramptoTime(data.reviewTime)),
  309. kv('拒绝原因', data.rejectReason)
  310. ].join('')),
  311. notice('如有疑问,请通过 RunForge 联系客服处理。', 'neutral'),
  312. pEsc('感谢您的理解。', { last: true })
  313. ].join('')
  314. await sendEmail(
  315. email,
  316. 'RunForge|赠送次数审核未通过',
  317. renderEmail({
  318. pageTitle: '赠送次数审核未通过',
  319. headline: '赠送次数审核未通过',
  320. variant: 'warning',
  321. bodyHtml
  322. })
  323. )
  324. }
  325. async powerCheck(email, data) {
  326. const gap = Number(data.lowest) - Number(data.now_balance)
  327. const gapStr = Number.isFinite(gap) ? gap.toFixed(2) : ''
  328. const bodyHtml = [
  329. pEsc('尊敬的用户:'),
  330. pEsc(`${data.building}${data.room} 宿舍电费已低于您设定的提醒阈值,请关注余额并及时充值。`),
  331. panel([
  332. kv('校区', data.area),
  333. kv('楼栋', data.building),
  334. kv('寝室号', data.room),
  335. kv('当前余额', `¥${data.now_balance}`),
  336. kv('提醒阈值', `¥${data.lowest}`),
  337. kv('最近扣费时间', data.now_change_time)
  338. ].join('')),
  339. notice(
  340. gapStr !== ''
  341. ? `当前余额已低于提醒阈值 ${gapStr} 元,建议尽快充值以免影响用电。`
  342. : '当前余额已低于提醒阈值,建议尽快充值以免影响用电。',
  343. 'warning'
  344. ),
  345. pEsc('您可在 RunForge 中调整提醒阈值或管理电费订阅。', { last: true })
  346. ].join('')
  347. await sendEmail(
  348. email,
  349. 'RunForge|宿舍电费提醒',
  350. renderEmail({
  351. pageTitle: '宿舍电费提醒',
  352. headline: '宿舍电费余额提醒',
  353. variant: 'warning',
  354. bodyHtml
  355. })
  356. )
  357. }
  358. formatSecondsToMinSec(totalSeconds) {
  359. const minutes = Math.floor(totalSeconds / 60)
  360. const seconds = totalSeconds % 60
  361. return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
  362. }
  363. calculatePace(seconds, kilometers) {
  364. const paceInSeconds = seconds / kilometers
  365. const minutes = Math.floor(paceInSeconds / 60)
  366. const remainingSeconds = Math.round(paceInSeconds % 60)
  367. return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`
  368. }
  369. }
  370. const EmailTemplate = new emailTemplate()
  371. module.exports = EmailTemplate