package top.r3944realms.ltdmanager.module import kotlinx.coroutines.* import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse import top.r3944realms.ltdmanager.core.mail.mail import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.MessageElement import top.r3944realms.ltdmanager.napcat.data.MessageType import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil import top.r3944realms.ltdmanager.utils.LoggerUtil import java.io.File import java.sql.Timestamp import java.util.* import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock /* 1. 订阅消息模块 (触发关键词, 注意过滤自己的消息,避免重复触发) [Done] 2. 根据QQ号去查询机器人数据库中的视图表的id (此操作耗时,应设置针对指定用户的持久化冷却) 3. id存在 [错误处理: id不存在提醒用户为无法查询到你的id,请联系管理员检查状态] i. effective 和 is_used 均为 1, 则回复提醒你已经使用了你的邀请码,切勿重复发送 ii. effective 为 1 且 is_used 为 0 则查询token_id对应的token记录然后构造发送邮件 提醒用户邮件已发送 iii. effective 为 0 则先通过API创建Token 获取来的响应 [错误处理: 当获取的json消息解析中success为false,则回复用户message中的错误信息] 用Token去邀请码数据库中查询token_id,将其记录在机器人数据库对应白名单id映射token_id表中 [存在则更新,不存在则插入], 然后按ii.执行 */ /* api格式 https://skins.r3944realms.top/api/invitation-codes/generate?token=XXXX&amount=1 成功消息: { "success": true, "message": "邀请码生成成功", "data": [ { "code": "XXXXXXX", "generated_at": "2025-08-29T09:36:36.910623Z", "expires_at": "2025-09-05T09:36:36.910506Z" } ] } 失败消息: { "success": false, "message": "无效的 API Token" } */ class InvitationCodesModule( private val groupMessagePollingModule: GroupMessagePollingModule, private val mailModule: MailModule, private val apiToken: String, private val selfId: Long, private val cooldownMillis: Long = 120_000, private val keywords: Set = setOf("申请邀请码") ) : BaseModule(), PersistentState { override val name: String = "InvitationCodesModule" private var scope: CoroutineScope? = null // 持久化文件(带锁 + 备份) private val stateFile = File("invitation_codes_quarry_state.json") private val stateBackupFile = File("invitation_codes_quarry_state.json.bak") private val fileLock = ReentrantLock() private var lastTriggerMapState = loadState() override fun getStateFile(): File = stateFile override fun getState(): LastTriggerMapState = lastTriggerMapState override fun onLoad() { LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}") LoggerUtil.logger.info("[$name] 上次触发状态: lastTriggerMap=${lastTriggerMapState.map}") LoggerUtil.logger.info("[$name] 关键词列表: $keywords") scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope!!.launch { LoggerUtil.logger.info("[$name] 轮询协程启动") groupMessagePollingModule.messagesFlow.collect { messages -> if (loaded) handleMessages(messages) } } // 定时落盘(防止异常退出丢状态) scope!!.launch { while (isActive) { delay(60_000) // 每分钟保存一次 saveState(lastTriggerMapState) } } } override suspend fun onUnload() { LoggerUtil.logger.info("[$name] 模块卸载,保存状态...") saveState(lastTriggerMapState) LoggerUtil.logger.info("[$name] 模块卸载,取消协程...") scope?.cancel() LoggerUtil.logger.info("[$name] 模块已卸载完成") } // ========================= // 消息处理主流程 // ========================= private suspend fun handleMessages(messages: List) { val triggerMsgs = filterTriggerMessages(messages) if (triggerMsgs.isEmpty()) return try { val hadValidCodeButNotUsed = mutableListOf>() val needNewCode = mutableListOf>() getIdAndSelectSituation(triggerMsgs, hadValidCodeButNotUsed, needNewCode) createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode) hadVaildCodeButNotUseListHandler(hadValidCodeButNotUsed + needNewCode) } catch (e: Exception) { sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: $e") } finally { saveState(lastTriggerMapState) } } /** 过滤出符合条件的触发消息 */ private fun filterTriggerMessages(messages: List) : List { val filtered = messages.asSequence() .filter { msg -> msg.userId != selfId && (msg.time > lastTriggerMapState.getLastTriggerTime(msg.userId) || (msg.time == lastTriggerMapState.getLastTriggerTime(msg.userId) && msg.realId > lastTriggerMapState.getLastTriggerRealId(msg.userId))) && msg.message.any { seg -> seg.type == MessageType.Text && seg.data.text?.let { text -> keywords.any { keyword -> text == keyword } } == true } } .groupBy { it.userId } .mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } } .filter { runBlocking { filterCoolDownMessage(it) } } .toList() if (filtered.isNotEmpty()) { LoggerUtil.logger.info("[$name] 待处理消息队列: $filtered") } return filtered } private suspend fun getIdAndSelectSituation(msgs: List, hadVaildCodeButNotUseList : MutableList>, needNewCodeList: MutableList>) { if (msgs.isEmpty()) return val qqIds = msgs.map { it.userId } val placeholders = java.lang.String.join(",", Collections.nCopies(qqIds.size, "?")) // 修正SQL语句的表名引用 val sql = """ SELECT q.player_id, q.effective, q.is_used, q.qq FROM ltd_manager_bot.qualified_user_info q WHERE q.qq IN ($placeholders) """.trimIndent() try { getConnection().use { conn -> conn.prepareStatement(sql).use { pstmt -> // 设置所有参数 for (i in qqIds.indices) { pstmt.setLong(i + 1, qqIds[i]) } pstmt.executeQuery().use { rs -> // 创建结果映射表 val resultMap = mutableMapOf>() while (rs.next()) { val qq = rs.getLong("qq") val playerId = rs.getLong("player_id") // 处理可能的null值 val playerIdValue = if (rs.wasNull()) null else playerId val effective = rs.getBoolean("effective") val isUsed = rs.getBoolean("is_used") resultMap[qq] = Triple(playerIdValue, effective, isUsed) } // 分类处理每个消息 for (msg in msgs) { val result = resultMap[msg.userId] when { result == null -> { // 数据库中没有记录, 属于是异常 LoggerUtil.logger.error("[$name] 无法查询该QQ号为:${msg.userId}的白名单ID,可能该用户非白名单成员") sendFailedMessage(napCatClient, msg.userId, msg.realId, msg.time, "无法查询到你的白名单应用id,请联系管理员检查状态,对应QQ号:${msg.userId}") } result.first != null && result.second == true && result.third == true -> { // 有player_id且已使用 LoggerUtil.logger.info("[$name] 该QQ号为:${msg.userId}的白名单ID是${result.first},已使用对应激活码") sendMessage(napCatClient, msg.userId, msg.realId, msg.time, "你已经使用了你的邀请码,切勿重复发送") } result.first != null && result.second == true && result.third == false -> { // 有player_id、有效且未使用 LoggerUtil.logger.info("[$name] 该QQ号为:${msg.userId}的白名单ID是${result.first},已有对应激活码但未使用") hadVaildCodeButNotUseList.add(result.first!! to msg) } result.first != null && result.second == false -> { // 没有player_id但有效,需要新code或处理 needNewCodeList.add(result.first!! to msg) } else -> { //其它情况,异常,不应该出现 sendFailedMessage(napCatClient, msg.userId, msg.realId, msg.time, "非法状态,请联系管理员:$result") } } } } } } } catch (e: Exception) { // 更好的错误处理 LoggerUtil.logger.error("[$name] 批量查询用户资格信息失败: ${e.message}", e) sendFailedMessage(napCatClient, text = "批量查询用户资格信息失败,请联系管理员: ${e.message}") } } private suspend fun hadVaildCodeButNotUseListHandler(list: List>) { if (list.isEmpty()) return val whiteListIds = list.map { it.first } val placeholders = java.lang.String.join(",", Collections.nCopies(whiteListIds.size, "?")) val sql = """ SELECT q.player_id, q.player_name, q.token, q.expires_at FROM ltd_manager_bot.qualified_user_info q WHERE q.player_id IN ($placeholders) """.trimIndent() try { getConnection().use { conn -> conn.prepareStatement(sql).use { pstmt -> for (i in whiteListIds.indices) { pstmt.setLong(i + 1, whiteListIds[i]) } pstmt.executeQuery().use { rs -> val resultMap = mutableMapOf>() while (rs.next()) { val playerId = rs.getLong("player_id") val playerName = rs.getString("player_name") val token = rs.getString("token") val tokenValue = if (rs.wasNull()) null else token val expiresAt = rs.getTimestamp("expires_at") val expiresAtValue = if (rs.wasNull()) null else expiresAt resultMap[playerId] = Triple(playerName, tokenValue, expiresAtValue) } // 直接遍历原始列表,不需要额外的映射 for ((playerId, specificMsg) in list) { val mailRequestArgument = resultMap[playerId] if (mailRequestArgument?.second != null && mailRequestArgument.third != null) { mailModule.enqueue(mail { to += specificMsg.userId.toString() + "@qq.com" // 直接使用 specificMsg // 根据需要配置邮件内容 subject = "LTD邀请码邮件" isHtml = true body = HtmlTemplateUtil.tokenMailHtmlTemplate( mailRequestArgument.first!!, mailRequestArgument.second!!, mailRequestArgument.third!!, 7,2025 ) }) sendMessage(napCatClient, specificMsg.userId, specificMsg.realId, specificMsg.time,"已发送邮件注意,查收QQ邮箱") } else if (mailRequestArgument?.second != null) { mailModule.enqueue(mail { to += specificMsg.userId.toString() + "@qq.com" // 直接使用 specificMsg // 根据需要配置邮件内容 subject = "LTD邀请码邮件" isHtml = true body = HtmlTemplateUtil.tokenMailHtmlTemplate( mailRequestArgument.first!!, mailRequestArgument.second!!, timeYear = 2025 ) }) sendMessage(napCatClient, specificMsg.userId, specificMsg.realId, specificMsg.time,"已发送邮件注意,查收QQ邮箱") } else { LoggerUtil.logger.error("[$name] 异常情况,code为 空值") sendFailedMessage(napCatClient, specificMsg.userId, specificMsg.realId, specificMsg.time, "系统内部异常,请联系管理员") } } } } } } catch (e: Exception) { LoggerUtil.logger.error("[$name] 查询已获取邀请码但未使用或未过期用户,Code信息失败: ${e.message}", e) sendFailedMessage(napCatClient, text = "查询已获取邀请码但未使用或未过期用户,Code信息失败: ${e.message}") } } private suspend fun sendMessage( client: NapCatClient, qq: Long, realId: Long, time: Long, text: String = "正常消息" ) { LoggerUtil.logger.info("[$name] 发送消息: realId=$realId, text=$text") val request = SendGroupMsgRequest( MessageElement.reply(ID.long(realId), text), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) LoggerUtil.logger.info("[$name] 已发送 消息") // 更新触发的最大 realId lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, time) } private suspend fun sendFailedMessage( client: NapCatClient, qq: Long? = null, realId: Long? = null, time: Long? = null, text: String = "失败消息" ) { LoggerUtil.logger.info("[$name] 发送失败消息: realId=$realId, text=$text") if (realId != null && qq != null && time != null) { val request = SendGroupMsgRequest( MessageElement.reply(ID.long(realId), text), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) LoggerUtil.logger.info("[$name] 已发送 失败消息") // 更新触发的最大 realId lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, time) } else { val request = SendGroupMsgRequest( listOf(MessageElement.text(text)), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]") } } // ========================= // 冷却逻辑 // ========================= private suspend fun filterCoolDownMessage(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { val triggerDetail = lastTriggerMapState.map[msg.userId] val lastTriggerTime = triggerDetail?.time ?: -1L val lastCooldownRealId = triggerDetail?.lastCooldownRealId ?: -1L val nowSec = System.currentTimeMillis() / 1000 // 转成秒 if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownMillis / 1000) { // 正常触发 return true } // 冷却中,如果本消息未发送过冷却提示 if (msg.realId != lastCooldownRealId) { val remaining = ((cooldownMillis / 1000) - (nowSec - lastTriggerTime)).coerceAtLeast(1) val msgText = "⏳ 申请邀请码过于频繁,请稍后再试(剩余 $remaining 秒)" sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText) // 记录这条消息已发送过冷却提示 lastTriggerMapState = lastTriggerMapState.updateLastCooldownRealId(msg.userId, msg.realId) } return false } private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, msg: String) { val request = SendGroupMsgRequest( MessageElement.reply(ID.long(realId), msg), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, -1) } private suspend fun createAndSearchInvitationCodeIdsThenUpdateDate( needNewTokenIdAndMsgPairs: List>, ) { if (needNewTokenIdAndMsgPairs.isEmpty()) return try { // 1. 创建邀请码 val invitationCodes = createInvitationCodes(needNewTokenIdAndMsgPairs.size) // 2. 验证数量匹配 validateCodeCountMatch(invitationCodes, needNewTokenIdAndMsgPairs) // 3. 获取邀请码ID val codeToIdMap = getInvitationCodeIds(invitationCodes!!.map { it.code }) // 4. 更新或插入关联关系 updateInvitationCodeAscription(needNewTokenIdAndMsgPairs.map { it.first }, codeToIdMap.values.toList()) } catch (e: Exception) { handleCreationError(e) } } /** * 1. 创建邀请码 */ private suspend fun createInvitationCodes(amount: Int): List? { return try { val response = blessingSkinClient.submitRequest( GenerateInvitationCodeRequest(amount = amount, token = apiToken) ) when (response) { is ResponseResult.Success -> { if (response.response.success) { response.response.data } else { LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}") null } } is ResponseResult.Failure -> { LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}") null } } } catch (e: Exception) { LoggerUtil.logger.error("[$name] 创建邀请码异常", e) null } } /** * 2. 验证数量匹配 */ private fun validateCodeCountMatch( invitationCodes: List?, needNewTokenIdAndMsgPairs: List> ) { if (invitationCodes == null) { throw InvitationCodeException.ApiFailureException("获取邀请码请求失败") } if (invitationCodes.size != needNewTokenIdAndMsgPairs.size) { throw InvitationCodeException.QuantityMismatchException( expectedCount = needNewTokenIdAndMsgPairs.size, actualCount = invitationCodes.size ) } } /** * 3. 获取邀请码ID */ private fun getInvitationCodeIds(invitationCodes: List): Map { if (invitationCodes.isEmpty()) return emptyMap() val placeholders = invitationCodes.joinToString(",") { "?" } val sql = """ SELECT i.id, i.code FROM blessingskin.invitation_codes i WHERE i.code IN ($placeholders) """.trimIndent() return getConnection().use { conn -> conn.prepareStatement(sql).use { pstmt -> // 设置参数 invitationCodes.forEachIndexed { index, code -> pstmt.setString(index + 1, code) } val resultMap = mutableMapOf() pstmt.executeQuery().use { rs -> while (rs.next()) { val id = rs.getLong("id") val code = rs.getString("code") resultMap[code] = id } } resultMap } } } /** * 4. 更新或插入关联关系 */ private fun updateInvitationCodeAscription(playerIds: List, codeIds: List) { if (playerIds.size != codeIds.size) { throw IllegalStateException("playerIds和codeIds数量不匹配: ${playerIds.size} vs ${codeIds.size}") } if (playerIds.isEmpty()) return val placeholders = playerIds.joinToString(",") { "(?, ?)" } val sql = """ INSERT INTO ltd_manager_bot.invitation_code_ascription (id, token_id) VALUES $placeholders ON DUPLICATE KEY UPDATE token_id = VALUES(token_id) """.trimIndent() getConnection().use { conn -> conn.prepareStatement(sql).use { pstmt -> var paramIndex = 1 for (i in playerIds.indices) { pstmt.setLong(paramIndex++, playerIds[i]) pstmt.setLong(paramIndex++, codeIds[i]) } val affectedRows = pstmt.executeUpdate() LoggerUtil.logger.debug("[$name] 更新了 $affectedRows 条关联记录") } } } /** * 5. 错误处理 */ private suspend fun handleCreationError(e: Exception) { when (e) { is InvitationCodeException -> { LoggerUtil.logger.error("[$name] ${e.message}") if (e is InvitationCodeException.QuantityMismatchException) { // 数量不匹配的特殊处理 handleQuantityMismatch(e.expectedCount, e.actualCount) } else { sendFailedMessage(napCatClient, text = "邀请码创建失败,请联系管理员") } } else -> { LoggerUtil.logger.error("[$name] 捕获异常", e) sendFailedMessage(napCatClient, text = "系统内部问题,请联系管理员") } } } /** * 数量不匹配的特殊处理 */ private suspend fun handleQuantityMismatch(expectedCount: Int, actualCount: Int) { LoggerUtil.logger.error( "[$name] 数量不一致BUG,期望: $expectedCount, 实际: $actualCount" ) sendFailedMessage(napCatClient, text = "系统内部BUG,请联系管理员") // TODO: 清理已创建的邀请码 cleanupCreatedInvitationCodes(actualCount) } /** * 清理已创建的邀请码(TODO实现) */ private fun cleanupCreatedInvitationCodes(createdCount: Int) { // 实现清理逻辑,删除多余的邀请码 LoggerUtil.logger.warn("[$name] 需要清理 $createdCount 个邀请码") } // ========================= // 状态持久化 // ========================= @Serializable data class LastTriggerMapState( val map: Map = emptyMap() ) { fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1 fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1 fun updateLastTrigger(qq: Long, realId: Long, time: Long = -1): LastTriggerMapState { val old = map[qq] val newTime = if (time != -1L) time else old?.time ?: -1 val newMap = map.toMutableMap().apply { put(qq, TriggerDetail(realId, newTime)) } return copy(map = newMap) } fun updateLastCooldownRealId(qq: Long, realId: Long): LastTriggerMapState { val old = map[qq] val newMap = map.toMutableMap().apply { put(qq, TriggerDetail( realId = old?.realId ?: -1, time = old?.time ?: -1, lastCooldownRealId = realId )) } return copy(map = newMap) } } @Serializable data class TriggerDetail(val realId : Long, val time: Long, val lastCooldownRealId: Long = -1L ) override fun loadState(): LastTriggerMapState { return try { if (!stateFile.exists()) { LoggerUtil.logger.info("[$name] 状态文件不存在,使用默认值") return LastTriggerMapState() } val content = stateFile.readText() val state = Json.decodeFromString(content) LoggerUtil.logger.info("[$name] 成功加载状态: ${state.map}, 文件路径=${stateFile.absolutePath}") state } catch (e: Exception) { LoggerUtil.logger.warn("[$name] 读取状态失败,尝试从备份恢复", e) try { if (stateBackupFile.exists()) { val backup = stateBackupFile.readText() val state = Json.decodeFromString(backup) LoggerUtil.logger.info("[$name] 成功从备份恢复状态: ${state.map}") state } else { LastTriggerMapState() } } catch (e2: Exception) { LoggerUtil.logger.error("[$name] 备份也损坏,使用默认值", e2) LastTriggerMapState() } } } override fun saveState(state: LastTriggerMapState) { fileLock.withLock { try { val json = Json.encodeToString(state) // 先写备份 if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true) // 写入新文件 stateFile.writeText(json) LoggerUtil.logger.info("[$name] 已保存状态: ${state.map}, 文件路径=${stateFile.absolutePath}") } catch (e: Exception) { LoggerUtil.logger.error("[$name] 保存状态失败", e) } } } }