SendCount.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. const API = require("../../lib/API")
  2. const db = require("../../plugin/DataBase/db")
  3. const AccessControl = require("../../lib/AccessControl")
  4. const { BaseStdResponse } = require("../../BaseStdResponse")
  5. const EmailTemplate = require("../../plugin/Email/emailTemplate")
  6. const { insertLedgerRecord } = require("../../lib/Lepao/CountLedger")
  7. class SendCount extends API {
  8. constructor() {
  9. super()
  10. this.setPath("/Goods/SendCount")
  11. this.setMethod("POST")
  12. }
  13. async onRequest(req, res) {
  14. let { uuid, session, username, count } = req.body
  15. username = typeof username === "string" ? username.trim() : username
  16. count = Number(count)
  17. count = Math.round(count * 100) / 100
  18. if ([uuid, session, username, count].some(v => v == null || v === "" || Number.isNaN(count)))
  19. return res.json({ ...BaseStdResponse.MISSING_PARAMETER })
  20. if (count <= 0 || count > 9999)
  21. return res.json({ ...BaseStdResponse.ERR, msg: "超出赠送的里程范围,请重新选择赠送公里数" })
  22. if (!(await AccessControl.checkSession(uuid, session)))
  23. return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED })
  24. const conn = await db.connect() // 这里直接拿 connection
  25. try {
  26. await conn.beginTransaction()
  27. const [senderRows] = await conn.execute(
  28. "SELECT id, username, lepao_count, COALESCE(send_count_auto_approve, 0) AS send_count_auto_approve FROM users WHERE uuid = ?",
  29. [uuid]
  30. )
  31. if (!senderRows || senderRows.length !== 1) {
  32. await conn.rollback()
  33. return res.json({ ...BaseStdResponse.MISSING_FILE, msg: "获取用户信息失败!" })
  34. }
  35. const [targetRows] = await conn.execute(
  36. "SELECT id, uuid FROM users WHERE username = ?",
  37. [username]
  38. )
  39. if (!targetRows || targetRows.length !== 1) {
  40. await conn.rollback()
  41. return res.json({ ...BaseStdResponse.ERR, msg: "未找到接收用户,请检查用户名是否正确!" })
  42. }
  43. if (targetRows[0].uuid === uuid) {
  44. await conn.rollback()
  45. return res.json({ ...BaseStdResponse.ERR, msg: "不能给自己赠送里程!" })
  46. }
  47. const [decResult] = await conn.execute(
  48. "UPDATE users SET lepao_count = lepao_count - ? WHERE uuid = ? AND lepao_count >= ?",
  49. [count, uuid, count]
  50. )
  51. if (decResult.affectedRows !== 1) {
  52. await conn.rollback()
  53. return res.json({ ...BaseStdResponse.ERR, msg: "剩余乐跑里程不足,请购买后再赠送!" })
  54. }
  55. const senderLepaoBefore = Number(senderRows[0].lepao_count || 0)
  56. const autoApprove = Number(senderRows[0].send_count_auto_approve) === 1
  57. if (autoApprove) {
  58. const [recvRows] = await conn.execute(
  59. "SELECT uuid, lepao_count FROM users WHERE id = ? FOR UPDATE",
  60. [targetRows[0].id]
  61. )
  62. if (!recvRows || recvRows.length !== 1) {
  63. await conn.rollback()
  64. return res.json({ ...BaseStdResponse.ERR, msg: "未找到接收用户,请检查用户名是否正确!" })
  65. }
  66. const receiverUuid = recvRows[0].uuid
  67. const beforeRecv = Number(recvRows[0].lepao_count || 0)
  68. const [incResult] = await conn.execute(
  69. "UPDATE users SET lepao_count = lepao_count + ? WHERE id = ?",
  70. [count, targetRows[0].id]
  71. )
  72. if (!incResult || incResult.affectedRows !== 1) {
  73. await conn.rollback()
  74. return res.json({ ...BaseStdResponse.ERR, msg: "接收方入账失败,请稍后再试!" })
  75. }
  76. const [insertResult] = await conn.execute(
  77. `INSERT INTO lepao_send_count_request
  78. (sender_uuid, receiver_user_id, count, status, created_at, reviewed_at, reviewer_uuid)
  79. VALUES (?, ?, ?, 'approved', NOW(), NOW(), NULL)`,
  80. [uuid, targetRows[0].id, count]
  81. )
  82. if (!insertResult || insertResult.affectedRows !== 1) {
  83. await conn.rollback()
  84. return res.json({ ...BaseStdResponse.ERR, msg: "记录赠送失败,请稍后再试!" })
  85. }
  86. const requestId = insertResult.insertId
  87. await insertLedgerRecord({
  88. executor: conn,
  89. userUuid: uuid,
  90. delta: -count,
  91. balanceBefore: senderLepaoBefore,
  92. balanceAfter: senderLepaoBefore - count,
  93. bizType: "gift_send_lock",
  94. bizId: `send_request:${requestId}`,
  95. remark: `向${username}赠送${count}公里`
  96. })
  97. await insertLedgerRecord({
  98. executor: conn,
  99. userUuid: receiverUuid,
  100. delta: count,
  101. balanceBefore: beforeRecv,
  102. balanceAfter: beforeRecv + count,
  103. bizType: "gift_receive",
  104. bizId: `send_request:${requestId}`,
  105. operatorUuid: null,
  106. remark: `${senderRows[0].username}赠送${count}公里`
  107. })
  108. await conn.commit()
  109. const reviewTime = new Date().getTime()
  110. Promise.resolve().then(async () => {
  111. try {
  112. const infoSql = `
  113. SELECT ru.email AS receiver_email, ru.username AS receiver_username
  114. FROM users ru
  115. WHERE ru.id = ?
  116. `
  117. const infoRows = await db.query(infoSql, [targetRows[0].id])
  118. if (!infoRows || infoRows.length !== 1 || !infoRows[0].receiver_email) {
  119. this.logger.warn(`[SendCountNotify][auto][requestId=${requestId}] 接收人邮箱为空,跳过通知`)
  120. return
  121. }
  122. await EmailTemplate.sendCountRequestApproved(infoRows[0].receiver_email, {
  123. requestId,
  124. senderUsername: senderRows[0].username,
  125. count,
  126. reviewTime
  127. })
  128. } catch (mailErr) {
  129. this.logger.error(`[SendCountNotify][auto][requestId=${requestId}] 接收人通知发送失败:${mailErr.message || "未知错误"}`)
  130. }
  131. })
  132. return res.json({ ...BaseStdResponse.OK, msg: "赠送成功,对方已到账" })
  133. }
  134. const [insertResult] = await conn.execute(
  135. `INSERT INTO lepao_send_count_request
  136. (sender_uuid, receiver_user_id, count, status, created_at)
  137. VALUES (?, ?, ?, 'pending', NOW())`,
  138. [uuid, targetRows[0].id, count]
  139. )
  140. if (!insertResult || insertResult.affectedRows !== 1) {
  141. await conn.rollback()
  142. return res.json({ ...BaseStdResponse.ERR, msg: "提交赠送审核失败,请稍后再试!" })
  143. }
  144. const requestId = insertResult.insertId
  145. await insertLedgerRecord({
  146. executor: conn,
  147. userUuid: uuid,
  148. delta: -count,
  149. balanceBefore: senderLepaoBefore,
  150. balanceAfter: senderLepaoBefore - count,
  151. bizType: "gift_send_lock",
  152. bizId: `send_request:${requestId}`,
  153. remark: `向${username}赠送${count}次`
  154. })
  155. await conn.commit()
  156. const createTime = new Date().getTime()
  157. // 非阻塞通知管理员,不影响主业务流程
  158. Promise.resolve().then(async () => {
  159. try {
  160. const adminSql = `
  161. SELECT email
  162. FROM users
  163. WHERE email IS NOT NULL
  164. AND email <> ''
  165. AND (JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"'))
  166. `
  167. const adminRows = await db.query(adminSql)
  168. if (!adminRows || adminRows.length === 0) {
  169. this.logger.warn(`[SendCountNotify][submit][requestId=${requestId}] 未找到可通知的管理员邮箱`)
  170. return
  171. }
  172. const emails = [...new Set(adminRows.map(row => row.email).filter(Boolean))]
  173. for (const email of emails) {
  174. await EmailTemplate.sendCountRequestNotifyAdmins(email, {
  175. requestId,
  176. senderUsername: senderRows[0].username,
  177. receiverUsername: username,
  178. count,
  179. createTime
  180. })
  181. }
  182. } catch (mailErr) {
  183. this.logger.error(`[SendCountNotify][submit][requestId=${requestId}] 管理员通知发送失败:${mailErr.message || "未知错误"}`)
  184. }
  185. })
  186. return res.json({ ...BaseStdResponse.OK, msg: "已提交审核,审核通过后接收方将到账" })
  187. } catch (err) {
  188. try { await conn.rollback() } catch (_) { }
  189. this.logger.error(`赠送乐跑里程失败!${err.message || "未知错误"}`)
  190. return res.json({
  191. ...BaseStdResponse.ERR,
  192. msg: `赠送里程失败,请稍后再试!`
  193. })
  194. } finally {
  195. if (conn?.connection && typeof conn.connection.release === 'function' && typeof conn?.release === 'function') {
  196. conn.release()
  197. }
  198. }
  199. }
  200. }
  201. module.exports.SendCount = SendCount