MailManager.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. --邮件管理
  2. local LuaMongo = _G.lua_mongo
  3. local Log = require("common.Log")
  4. local DB = require("common.DB")
  5. local Util = require("common.Util")
  6. local Msg = require("core.Msg")
  7. local ObjHuman = require("core.ObjHuman")
  8. local RoleSystemLogic = require("roleSystem.RoleSystemLogic")
  9. local RoleSystemDefine = require("roleSystem.RoleSystemDefine")
  10. local RoleDBLogic = require("role.RoleDBLogic")
  11. local MailExcel = require("excel.mail")
  12. local CommonDB = require("common.CommonDB")
  13. SYSTEM = 1 -- 系统邮件
  14. GONGGAO = 2 -- 公告
  15. MAIL_MAX_CNT = 100 -- 邮件最大值
  16. MAIL_LOOKBACK_SEC = 90 * 86400 -- 查询回溯上限(覆盖较长 expireTime)
  17. MAIL_SCAN_LIMIT = 2000 -- 单时间窗游标扫描上限
  18. MAIL_SORT_DESC = {time = -1} -- find 第4参: 按时间倒序,优先拿到最新邮件
  19. local FIELD_ID = {_id = nil}
  20. local FIELD_RECEIVER = {uuid = nil,type = nil}
  21. --后面如果fbAccount有值应该把区服id一起
  22. function add(type,receiverUuid,title,content,items,senderName,sender,time,fbTime,fbContent,fbAccount,expireTime)
  23. if _G.is_middle == true or type == GONGGAO then
  24. return
  25. end
  26. local mail = {}
  27. mail.type = type
  28. mail.receiverUuid = receiverUuid
  29. mail.title = title
  30. mail.content = content
  31. mail.read = nil
  32. mail.senderName = title
  33. mail.head = 0
  34. mail.fbTime = fbTime
  35. mail.fbContent = fbContent
  36. --后面用到
  37. local fields = {head = 1,lv = 1}
  38. if fbAccount ~= nil then
  39. local db = RoleDBLogic.getDbByAccount(fbAccount,fields)
  40. mail.fbHead = db.head
  41. mail.fbLv = db.lv
  42. end
  43. if sender and sender.db and sender.db.identity then
  44. mail.identity = sender.db.identity
  45. end
  46. if sender and sender.db and sender.db.head then
  47. mail.head = sender.db.head
  48. end
  49. if time == nil then
  50. mail.time = os.time()
  51. else
  52. mail.time = time
  53. end
  54. if expireTime then
  55. mail.expireTime = expireTime
  56. end
  57. if items then
  58. mail.items = {}
  59. for _,item in ipairs(items) do
  60. mail.items[#mail.items + 1] = {item[1],item[2]}
  61. end
  62. end
  63. if items == nil then
  64. mail.get = 1
  65. mail.flag = nil
  66. else
  67. mail.get = nil
  68. mail.flag = 1
  69. end
  70. LuaMongo.insert(DB.db_mail, mail)
  71. -- 检测红点会调用getMails(), 短时间插入多封邮件加上玩家邮件数量过多,会发生段错误
  72. -- RoleSystemLogic.onDotByUuid(receiverUuid, RoleSystemDefine.ROLE_SYS_ID_204)
  73. return mail
  74. end
  75. -- condition = {startLv = 1, endLv = 100, lastLoginTime = xxx}
  76. local MailLvQuery = {lv = {["$gte"] = 0, ["$lte"] = 0}}
  77. local MailLvField = {lastLoginTime = 1, lastLogoutTime = 1}
  78. function sendMailByCondition(condition, title, content, gridList,fbTime,fbContent,fbAccount,expireTime)
  79. local startLv = condition.startLv and tonumber(condition.startLv)
  80. local endLv = condition.endLv and tonumber(condition.endLv)
  81. local lastLoginTime = condition.lastLoginTime and tonumber(condition.lastLoginTime) or 0
  82. if startLv == nil or endLv == nil or lastLoginTime == nil then
  83. return
  84. end
  85. local successCount = 0
  86. local failCount = 0
  87. --Log.write(Log.LOGID_ERR_PCALL, "mail sendMailByCondition" , "")
  88. -- 在线的只要等级符合直接发
  89. for uuid, oHuman in pairs(ObjHuman.onlineUuid) do
  90. if oHuman.db.lv >= startLv and
  91. oHuman.db.lv <= endLv then
  92. p_ret, err = pcall(add, SYSTEM, oHuman.db._id, title, content, gridList, condition.senderName, nil, nil,fbTime,fbContent,fbAccount,expireTime)
  93. if p_ret then
  94. successCount = successCount + 1
  95. --Log.write(Log.LOGID_ERR_PCALL, "mail rolename=" .. oHuman.db.name, err)
  96. else
  97. failCount = failCount + 1
  98. --Log.write(Log.LOGID_ERR_PCALL, "mail rolename=" .. oHuman.db.name, err)
  99. end
  100. end
  101. end
  102. -- 不在线的,还需要验证x天内登录
  103. MailLvQuery.lv["$gte"] = startLv
  104. MailLvQuery.lv["$lte"] = endLv
  105. LuaMongo.find(DB.db_char, MailLvQuery, MailLvField)
  106. while true do
  107. local data = {}
  108. if not LuaMongo.next(data) then
  109. break
  110. end
  111. if not ObjHuman.onlineUuid[data._id] and
  112. (lastLoginTime == 0 or
  113. (data.lastLoginTime and data.lastLoginTime >= lastLoginTime) or
  114. (data.lastLogoutTime and data.lastLogoutTime >= lastLoginTime)
  115. ) then
  116. p_ret, err = pcall(add, SYSTEM, data._id, title, content, gridList, condition.senderName, nil, nil,fbTime,fbContent,fbAccount)
  117. if p_ret then
  118. successCount = successCount + 1
  119. else
  120. failCount = failCount + 1
  121. Log.write(Log.LOGID_ERR_PCALL, "mail role uuid=" .. data._id, err)
  122. end
  123. end
  124. end
  125. return successCount, failCount
  126. end
  127. function del(mailUuid)
  128. FIELD_ID._id = mailUuid
  129. LuaMongo.remove(DB.db_mail, FIELD_ID)
  130. end
  131. function getMail(mailUuid)
  132. FIELD_ID._id = mailUuid
  133. LuaMongo.find(DB.db_mail,FIELD_ID)
  134. local mail = {}
  135. if not LuaMongo.next(mail) then
  136. return nil
  137. end
  138. return mail
  139. end
  140. local function cmpMail(a, b)
  141. return a.time > b.time
  142. end
  143. local mails = {}
  144. local seenMailIds = {}
  145. local function isMailExpired(m, now)
  146. if type(m.time) ~= "number" then
  147. m.time = 0
  148. end
  149. local expireTime = m.expireTime
  150. if expireTime and type(expireTime) == "number" then
  151. return m.time <= now - expireTime
  152. end
  153. return m.time <= now - 7 * 86400
  154. end
  155. local function countValidThreshold(sortedMails, now)
  156. local cnt = 0
  157. for _, m in ipairs(sortedMails) do
  158. if not isMailExpired(m, now) then
  159. cnt = cnt + 1
  160. if cnt >= MAIL_MAX_CNT then
  161. return cnt, m.time
  162. end
  163. end
  164. end
  165. return cnt, nil
  166. end
  167. -- 缓存过大时只保留最新 MAIL_MAX_CNT 条未过期,降低内存与排序成本
  168. local function trimTempMails(tempMails, now)
  169. if #tempMails <= MAIL_MAX_CNT * 2 then
  170. return
  171. end
  172. table.sort(tempMails, cmpMail)
  173. local writeIdx = 0
  174. for _, m in ipairs(tempMails) do
  175. if not isMailExpired(m, now) then
  176. writeIdx = writeIdx + 1
  177. tempMails[writeIdx] = m
  178. if writeIdx >= MAIL_MAX_CNT then
  179. break
  180. end
  181. end
  182. end
  183. for i = writeIdx + 1, #tempMails do
  184. tempMails[i] = nil
  185. end
  186. end
  187. local function buildMailQuery(receiverUuid, mailType, startTime, endTime, useLte)
  188. local query = {receiverUuid = receiverUuid}
  189. if mailType ~= nil then
  190. query.type = mailType
  191. end
  192. local timeCond = {["$gte"] = startTime}
  193. if useLte then
  194. timeCond["$lte"] = endTime
  195. else
  196. timeCond["$lt"] = endTime
  197. end
  198. query.time = timeCond
  199. return query
  200. end
  201. -- function getMails(receiverUuid,mailType)
  202. -- for key in ipairs(mails) do
  203. -- mails[key] = nil
  204. -- end
  205. -- FIELD_RECEIVER.receiverUuid = receiverUuid
  206. -- FIELD_RECEIVER.type = mailType
  207. -- LuaMongo.find(DB.db_mail,{["$query"]=FIELD_RECEIVER})
  208. -- local lastTime = os.time() - 7 * 86400
  209. -- local mailCnt = 0
  210. -- while true do
  211. -- local mail = {}
  212. -- -- if not LuaMongo.next(mail) then
  213. -- -- break
  214. -- -- end
  215. -- local res, err = pcall(function ()
  216. -- return LuaMongo.next(mail)
  217. -- end)
  218. -- if not res then
  219. -- Log.write(Log.LOGID_DEBUG, "MailManager.getMails err = ".. err)
  220. -- break
  221. -- end
  222. -- if not err then
  223. -- break
  224. -- end
  225. -- if mail.expireTime then -- 有指定过期时间
  226. -- if mail.time > os.time() - mail.expireTime then
  227. -- mailCnt = mailCnt + 1
  228. -- mails[mailCnt] = mail
  229. -- end
  230. -- elseif mail.time > lastTime then -- 没有就用默认过期时间
  231. -- mailCnt = mailCnt + 1
  232. -- mails[mailCnt] = mail
  233. -- end
  234. -- end
  235. -- if mailCnt > MAIL_MAX_CNT then
  236. -- table.sort(mails, cmpMail)
  237. -- for i = MAIL_MAX_CNT + 1, mailCnt do
  238. -- mails[i] = nil
  239. -- end
  240. -- end
  241. -- return mails
  242. -- end
  243. -- 分段 + 倒序游标:兼顾 C 驱动稳定性与“取最新 100 封未过期”语义
  244. function getMails(receiverUuid, mailType)
  245. for i = 1, #mails do
  246. mails[i] = nil
  247. end
  248. for k in pairs(seenMailIds) do
  249. seenMailIds[k] = nil
  250. end
  251. local now = os.time()
  252. local mailCnt = 0
  253. local tempMails = {}
  254. local thresholdTime = nil
  255. local lookbackStart = now - MAIL_LOOKBACK_SEC
  256. -- 半开区间 [start, end);首窗含 now 用 lte
  257. local timeWindows = {
  258. {now - 7 * 86400, now, true},
  259. {now - 14 * 86400, now - 7 * 86400, false},
  260. {now - 30 * 86400, now - 14 * 86400, false},
  261. {now - 60 * 86400, now - 30 * 86400, false},
  262. {now - MAIL_LOOKBACK_SEC, now - 60 * 86400, false},
  263. }
  264. local serverOpenTime = CommonDB.getServerOpenTime()
  265. if serverOpenTime < lookbackStart then
  266. timeWindows[#timeWindows + 1] = {serverOpenTime, lookbackStart, false}
  267. end
  268. for _, window in ipairs(timeWindows) do
  269. local startTime, endTime, useLte = window[1], window[2], window[3]
  270. if startTime >= endTime then
  271. goto continue_window
  272. end
  273. if thresholdTime then
  274. local windowMaxTime = useLte and endTime or (endTime - 1)
  275. if windowMaxTime < thresholdTime then
  276. break
  277. end
  278. end
  279. local query = buildMailQuery(receiverUuid, mailType, startTime, endTime, useLte)
  280. LuaMongo.find(DB.db_mail, query, nil, MAIL_SORT_DESC)
  281. local mail = {}
  282. local scanCnt = 0
  283. while true do
  284. local ok, hasNext = pcall(function()
  285. return LuaMongo.next(mail)
  286. end)
  287. if not ok then
  288. Log.write(Log.LOGID_DEBUG, "MailManager.getMails next err: " .. tostring(hasNext))
  289. break
  290. end
  291. if not hasNext then
  292. break
  293. end
  294. scanCnt = scanCnt + 1
  295. if scanCnt > MAIL_SCAN_LIMIT then
  296. Log.write(Log.LOGID_DEBUG, string.format(
  297. "Mail segment limit hit! uuid=%s window=%d~%d",
  298. receiverUuid, startTime, endTime))
  299. break
  300. end
  301. if thresholdTime and type(mail.time) == "number" and mail.time < thresholdTime then
  302. break
  303. end
  304. local mailId = mail._id
  305. if not mailId or not seenMailIds[mailId] then
  306. if mailId then
  307. seenMailIds[mailId] = true
  308. end
  309. local m = {}
  310. for k, v in pairs(mail) do
  311. m[k] = v
  312. end
  313. tempMails[#tempMails + 1] = m
  314. trimTempMails(tempMails, now)
  315. local validCnt, th = countValidThreshold(tempMails, now)
  316. if th then
  317. thresholdTime = th
  318. end
  319. end
  320. for k in pairs(mail) do
  321. mail[k] = nil
  322. end
  323. end
  324. ::continue_window::
  325. end
  326. table.sort(tempMails, cmpMail)
  327. for _, m in ipairs(tempMails) do
  328. if not isMailExpired(m, now) then
  329. mailCnt = mailCnt + 1
  330. mails[mailCnt] = m
  331. if mailCnt >= MAIL_MAX_CNT then
  332. break
  333. end
  334. end
  335. end
  336. return mails
  337. end
  338. function saveMail(mail)
  339. FIELD_ID._id = mail._id
  340. LuaMongo.update(DB.db_mail, FIELD_ID, mail)
  341. end
  342. function delAll(uuid,mailType)
  343. local QueryMailByUuid = {get = {["$exists"] = 0}, read = {["$exists"] = 1}}
  344. QueryMailByUuid.receiverUuid = uuid
  345. QueryMailByUuid.type = mailType
  346. QueryMailByUuid.get["$exists"] = 1
  347. LuaMongo.remove(DB.db_mail, QueryMailByUuid)
  348. end