Browse Source

✨ feat: 支持条件自动解绑重绑并放宽工单解绑限制

在新增乐跑账号时增加自动解绑重绑能力,满足学期前更新、自动乐跑关闭且当日次数未超限时自动完成解绑与绑定并记录审计;同时取消工单MCP的学期更新时间限制,仅保留自动乐跑开关与当日次数限制。

Made-with: Cursor
Pchen0 6 days ago
parent
commit
9c3f145cf4
2 changed files with 65 additions and 17 deletions
  1. 64 4
      apis/Lepao/Account/AddAccount.js
  2. 1 13
      lib/Lepao/WorkOrderMcp.js

+ 64 - 4
apis/Lepao/Account/AddAccount.js

@@ -1,5 +1,6 @@
 const API = require("../../../lib/API.js");
 const db = require("../../../plugin/DataBase/db.js");
+const Redis = require("../../../plugin/DataBase/Redis.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
 const { insertBindAudit, BindAuditAction, BindAuditSource } = require("../../../lib/Lepao/BindAudit.js");
@@ -13,6 +14,7 @@ class AddAccount extends API {
 
         this.emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
         this.banEmailList = ['icloud.com']
+        this.autoUnbindDailyLimit = 10
     }
 
     // 生成 6 位数字 + 字母混合码
@@ -36,6 +38,31 @@ class AddAccount extends API {
         }
     }
 
+    getSemesterStartTimestamp() {
+        const now = new Date()
+        const year = now.getFullYear()
+        const feb1ThisYear = new Date(year, 1, 1, 0, 0, 0, 0)
+        const aug31ThisYear = new Date(year, 7, 31, 0, 0, 0, 0)
+        if (now >= feb1ThisYear && now < aug31ThisYear) {
+            return feb1ThisYear.getTime()
+        }
+        return new Date(now < feb1ThisYear ? year - 1 : year, 7, 31, 0, 0, 0, 0).getTime()
+    }
+
+    getAutoUnbindDailyRedisKey() {
+        const now = new Date()
+        const year = now.getFullYear()
+        const month = `${now.getMonth() + 1}`.padStart(2, '0')
+        const day = `${now.getDate()}`.padStart(2, '0')
+        return `lepao:auto_unbind:daily:${year}${month}${day}`
+    }
+
+    getSecondsToDayEnd() {
+        const now = new Date()
+        const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0)
+        return Math.max(1, Math.floor((tomorrow.getTime() - now.getTime()) / 1000))
+    }
+
     async onRequest(req, res) {
         let { uuid, session, student_num, email, id, area, auto_time, auto_run, target_count, auto_day, notice_type, notes } = req.body
 
@@ -78,7 +105,7 @@ class AddAccount extends API {
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        let countSql = 'SELECT id, create_user, total_num FROM lepao_account WHERE student_num = ?'
+        let countSql = 'SELECT id, create_user, total_num, auto_run, update_time FROM lepao_account WHERE student_num = ?'
         let countRows = await db.query(countSql, [student_num])
 
         if (!countRows)
@@ -87,9 +114,18 @@ class AddAccount extends API {
         // 判断是否重复注册
         if (!id) {
             if (countRows.length !== 0 && countRows[0].create_user != null) {
-                if (countRows[0].create_user !== uuid)
-                    return res.json({ ...BaseStdResponse.ERR, msg: '该乐跑账号已被其他用户绑定,请联系客服解绑' })
-                return res.json({ ...BaseStdResponse.ERR, msg: '该乐跑账号您已绑定' })
+                if (countRows[0].create_user !== uuid) {
+                    const semesterStartTimestamp = this.getSemesterStartTimestamp()
+                    const dailyAutoUnbindKey = this.getAutoUnbindDailyRedisKey()
+                    const dailyAutoUnbindCount = Number(await Redis.get(dailyAutoUnbindKey) || 0)
+                    const canAutoUnbindAndRebind = (countRows[0].auto_run === 0) &&
+                        (!countRows[0].update_time || countRows[0].update_time < semesterStartTimestamp) &&
+                        (dailyAutoUnbindCount < this.autoUnbindDailyLimit)
+                    if (!canAutoUnbindAndRebind)
+                        return res.json({ ...BaseStdResponse.ERR, msg: '该乐跑账号已被其他用户绑定,请联系客服处理' })
+                } else {
+                    return res.json({ ...BaseStdResponse.ERR, msg: '该乐跑账号您已绑定' })
+                }
             }
         }
 
@@ -100,6 +136,10 @@ class AddAccount extends API {
 
         const time = new Date().getTime()
         const previousOwner = countRows.length !== 0 ? countRows[0].create_user : null
+        const shouldAutoUnbindAndRebind = !id &&
+            countRows.length !== 0 &&
+            previousOwner != null &&
+            previousOwner !== uuid
         const shouldRecordBind = !id && previousOwner !== uuid
 
         let sql, r
@@ -154,6 +194,26 @@ class AddAccount extends API {
                 })
 
                 if (shouldRecordBind) {
+                    if (shouldAutoUnbindAndRebind) {
+                        const unbindAuditOk = await insertBindAudit({
+                            studentNum: student_num,
+                            ownerUuid: previousOwner,
+                            action: BindAuditAction.PLATFORM_UNBIND,
+                            source: BindAuditSource.USER_API,
+                            operatorUuid: uuid,
+                            detail: { via: 'AddAccount:auto_unbind_rebind' },
+                            createdAt: time
+                        })
+                        if (!unbindAuditOk) {
+                            this.logger.warn(`自动解绑审计写入失败 student_num=${student_num}`)
+                        } else {
+                            const dailyAutoUnbindKey = this.getAutoUnbindDailyRedisKey()
+                            const latestAutoUnbindCount = await Redis.incr(dailyAutoUnbindKey)
+                            if (latestAutoUnbindCount === 1) {
+                                await Redis.expire(dailyAutoUnbindKey, this.getSecondsToDayEnd())
+                            }
+                        }
+                    }
                     const auditOk = await insertBindAudit({
                         studentNum: student_num,
                         ownerUuid: uuid,

+ 1 - 13
lib/Lepao/WorkOrderMcp.js

@@ -107,17 +107,6 @@ class WorkOrderMcp {
         }
     }
 
-    getSemesterTimestamps() {
-        const now = new Date()
-        const year = now.getFullYear()
-        const feb1ThisYear = new Date(year, 1, 1, 0, 0, 0, 0)     // 当年 2-01
-        const aug31ThisYear = new Date(year, 7, 31, 0, 0, 0, 0)  // 当年 8-31
-        // 下学期:2 月 1 日 ~ 8 月 31 日
-        // 上学期:8 月 31 日 ~ 次年 2 月 1 日
-        // 1 月属于上一年的上学期
-        return [(now >= feb1ThisYear && now < aug31ThisYear) ? feb1ThisYear.getTime() : new Date(now < feb1ThisYear ? year - 1 : year, 7, 31, 0, 0, 0, 0).getTime(), now.getTime() + 86400000]
-      }
-
     getDailyUnbindRedisKey(sender) {
         const now = new Date()
         const year = now.getFullYear()
@@ -142,12 +131,11 @@ class WorkOrderMcp {
             const dailyUnbindCount = Number(await Redis.get(dailyUnbindKey) || 0)
             if (dailyUnbindCount >= this.dailyUnbindLimit) return '今日通过MCP解绑次数已超过限制,只能人工处理'
 
-            let selectSql = `SELECT create_user, auto_run, update_time FROM lepao_account WHERE student_num = ? AND create_user IS NOT NULL`
+            let selectSql = `SELECT create_user, auto_run FROM lepao_account WHERE student_num = ? AND create_user IS NOT NULL`
             let selectRows = await db.query(selectSql, [student_num])
             if (!selectRows || selectRows.length === 0 || !selectRows[0]?.create_user) return '该账号未绑定,无需解绑'
 
             if (selectRows[0].auto_run === 1) return '该账号已开启自动乐跑,请用原绑定账号关闭自动乐跑后再解绑或联系人工客服处理'
-            if (selectRows[0].update_time && selectRows[0].update_time > this.getSemesterTimestamps()[0]) return '该账号在本学期存在更新记录,请等待人工处理'
             let updateSql = `UPDATE lepao_account SET create_user = NULL, auto_run = 0, update_time = ? WHERE student_num = ?`
             let updateRows = await db.query(updateSql, [Date.now(), student_num])
             if (!updateRows || updateRows.affectedRows !== 1)