Browse Source

: 新增乐跑距离配速等配置

Pchen0 3 weeks ago
parent
commit
83921190ef

+ 8 - 3
apis/Corn/StartAutoLepao.js

@@ -8,6 +8,7 @@ const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
 
 const { BaseStdResponse } = require("../../BaseStdResponse");
 const { planJkesAutoRun } = require('../../plugin/jkes/monthPolicy')
+const { buildAutoRunTaskExtrasFromAccountRow } = require('../../plugin/jkes/autoRunAccountOptions')
 
 class StartAutoLepao extends API {
     constructor() {
@@ -28,7 +29,8 @@ class StartAutoLepao extends API {
             const hour = new Date().getHours()
             this.logger.info('开始执行自动乐跑任务')
             let sql = `
-                        SELECT name, student_num, auto_day, token, target_count
+                        SELECT name, student_num, auto_day, token, target_count,
+                               auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km
                         FROM lepao_account
                         WHERE auto_run = 1
                             AND (auto_time = ? OR (auto_time = -1 AND today_auto_time = ?))
@@ -54,6 +56,7 @@ class StartAutoLepao extends API {
 
             for (const item of r) {
                 const { name, student_num, auto_day, token, target_count } = item
+                const runExtras = buildAutoRunTaskExtrasFromAccountRow(item)
 
                 const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(student_num))
                 if (isSuccess) {
@@ -79,8 +82,10 @@ class StartAutoLepao extends API {
                     data: {
                         taskId,
                         account: student_num,
-                        targetKm: plan.targetKm,
-                        autoDoubleSlot: plan.targetKm >= 2
+                        targetKm: runExtras.targetKm,
+                        autoDoubleSlot: runExtras.autoDoubleSlot,
+                        paceRandomMinSecPerKm: runExtras.paceRandomMinSecPerKm,
+                        paceRandomMaxSecPerKm: runExtras.paceRandomMaxSecPerKm
                     },
                     retry: 0
                 }

+ 8 - 3
apis/Corn/StartLepao.js

@@ -8,6 +8,7 @@ const {
 } = require('../../plugin/mq/runforgeTaskMq')
 const { BaseStdResponse } = require("../../BaseStdResponse")
 const { planJkesAutoRun } = require('../../plugin/jkes/monthPolicy')
+const { buildAutoRunTaskExtrasFromAccountRow } = require('../../plugin/jkes/autoRunAccountOptions')
 const jkesRedisKeys = require('../../plugin/jkes/redisKeys')
 
 class StartLepao extends API {
@@ -36,7 +37,8 @@ class StartLepao extends API {
 
             const day = new Date().getDay()
             let sql = `
-                        SELECT name, student_num, auto_day, token, target_count
+                        SELECT name, student_num, auto_day, token, target_count,
+                               auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km
                         FROM lepao_account
                         WHERE auto_run = 1 AND state = 1
                             AND JSON_CONTAINS(auto_day, CAST(? AS JSON))
@@ -48,6 +50,7 @@ class StartLepao extends API {
 
             for (const item of r) {
                 const { name, student_num, auto_day, token, target_count } = item
+                const runExtras = buildAutoRunTaskExtrasFromAccountRow(item)
                 this.logger.info(`${name}(${student_num})开始补充乐跑`)
 
                 const isSuccess = await Redis.get(jkesRedisKeys.lepaoSuccess(student_num))
@@ -76,8 +79,10 @@ class StartLepao extends API {
                         data: {
                             taskId,
                             account: student_num,
-                            targetKm: plan.targetKm,
-                            autoDoubleSlot: plan.targetKm >= 2
+                            targetKm: runExtras.targetKm,
+                            autoDoubleSlot: runExtras.autoDoubleSlot,
+                            paceRandomMinSecPerKm: runExtras.paceRandomMinSecPerKm,
+                            paceRandomMaxSecPerKm: runExtras.paceRandomMaxSecPerKm
                         },
                         retry: 0
                     }

+ 186 - 10
apis/Lepao/Account/AddAccount.js

@@ -2,6 +2,17 @@ const API = require("../../../lib/API.js");
 const db = require("../../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../../BaseStdResponse.js");
 const AccessControl = require("../../../lib/AccessControl.js");
+const { getJkesSettings } = require("../../../plugin/jkes/jkesSettings.js");
+const {
+    DEFAULT_AUTO_RUN_DISTANCE_KM,
+    validateAutoRunPresetForSave
+} = require("../../../plugin/jkes/autoRunAccountOptions.js");
+
+function pickBodyOrDb(bodyVal, dbVal, fallback) {
+    if (bodyVal !== undefined && bodyVal !== null && bodyVal !== "") return bodyVal;
+    if (dbVal !== undefined && dbVal !== null && dbVal !== "") return dbVal;
+    return fallback;
+}
 
 class AddAccount extends API {
     constructor() {
@@ -41,7 +52,25 @@ class AddAccount extends API {
     }
 
     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
+        let {
+            uuid,
+            session,
+            student_num,
+            email,
+            id,
+            area,
+            auto_time,
+            auto_run,
+            target_count,
+            auto_day,
+            notice_type,
+            notes,
+            auto_run_distance_km,
+            auto_run_distance_min_km,
+            auto_run_distance_max_km,
+            pace_min_sec_per_km,
+            pace_max_sec_per_km
+        } = req.body
 
         if ([uuid, session, student_num, auto_time, target_count, auto_day].some(value => value === '' || value === null || value === undefined))
             return res.json({
@@ -83,12 +112,88 @@ class AddAccount extends API {
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        let countSql = 'SELECT id, create_user, total_num, term_num FROM lepao_account WHERE student_num = ?'
+        let mergeForPreset = null
+        if (id) {
+            const ownerRows = await db.query(
+                "SELECT auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km FROM lepao_account WHERE id = ? AND create_user = ?",
+                [id, uuid]
+            );
+            if (!ownerRows || ownerRows.length === 0) {
+                return res.json({ ...BaseStdResponse.ERR, msg: "未找到该乐跑账号或无权限编辑" });
+            }
+            mergeForPreset = ownerRows[0];
+        }
+
+        let countSql =
+            "SELECT id, create_user, total_num, term_num, auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km FROM lepao_account WHERE student_num = ?";
         let countRows = await db.query(countSql, [student_num])
 
         if (!countRows)
             return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
 
+        if (!id && countRows.length > 0) {
+            mergeForPreset = countRows[0];
+        }
+
+        const cfg = getJkesSettings();
+        const paceDefLo = cfg.paceRandomMinSecPerKm ?? 180;
+        const paceDefHi = cfg.paceRandomMaxSecPerKm ?? 600;
+        const minMissing =
+            auto_run_distance_min_km === undefined ||
+            auto_run_distance_min_km === null ||
+            auto_run_distance_min_km === ''
+        const maxMissing =
+            auto_run_distance_max_km === undefined ||
+            auto_run_distance_max_km === null ||
+            auto_run_distance_max_km === ''
+        const legacyOnly =
+            minMissing &&
+            maxMissing &&
+            auto_run_distance_km !== undefined &&
+            auto_run_distance_km !== null &&
+            auto_run_distance_km !== ''
+
+        const presetBody = legacyOnly
+            ? {
+                  auto_run_distance_km,
+                  pace_min_sec_per_km: pickBodyOrDb(
+                      pace_min_sec_per_km,
+                      mergeForPreset?.pace_min_sec_per_km,
+                      paceDefLo
+                  ),
+                  pace_max_sec_per_km: pickBodyOrDb(
+                      pace_max_sec_per_km,
+                      mergeForPreset?.pace_max_sec_per_km,
+                      paceDefHi
+                  )
+              }
+            : {
+                  auto_run_distance_min_km: pickBodyOrDb(
+                      auto_run_distance_min_km,
+                      mergeForPreset?.auto_run_distance_min_km,
+                      DEFAULT_AUTO_RUN_DISTANCE_KM
+                  ),
+                  auto_run_distance_max_km: pickBodyOrDb(
+                      auto_run_distance_max_km,
+                      mergeForPreset?.auto_run_distance_max_km,
+                      DEFAULT_AUTO_RUN_DISTANCE_KM
+                  ),
+                  pace_min_sec_per_km: pickBodyOrDb(
+                      pace_min_sec_per_km,
+                      mergeForPreset?.pace_min_sec_per_km,
+                      paceDefLo
+                  ),
+                  pace_max_sec_per_km: pickBodyOrDb(
+                      pace_max_sec_per_km,
+                      mergeForPreset?.pace_max_sec_per_km,
+                      paceDefHi
+                  )
+              }
+        const preset = validateAutoRunPresetForSave(presetBody);
+        if (!preset.ok) {
+            return res.json({ ...BaseStdResponse.ERR, msg: preset.msg });
+        }
+
         // 判断是否重复注册
         if (!id) {
             if (countRows.length !== 0 && countRows[0].create_user != null) {
@@ -113,19 +218,60 @@ class AddAccount extends API {
         if (email && email.split('@')[1].toLowerCase() === 'qq.com' && this.isQQ(email.split('@')[0])) {
             user_avatar = `https://q2.qlogo.cn/headimg_dl?dst_uin=${email}&spec=640`
         } else {
-            user_avatar = userInfo.sex === 1 ? 'https://lepao-cloud.xxoo365.top/view.php/aee85ff43fd30d0df03c6a7dd9797d22.png' : 'https://lepao-cloud.xxoo365.top/view.php/fcb54dcc5e6209381e972ef73bdb4a93.png'
+            let userSex = 2;
+            const urows = await db.query("SELECT sex FROM users WHERE uuid = ? LIMIT 1", [uuid]);
+            if (urows?.[0]?.sex === 1) userSex = 1;
+            user_avatar =
+                userSex === 1
+                    ? "https://lepao-cloud.xxoo365.top/view.php/aee85ff43fd30d0df03c6a7dd9797d22.png"
+                    : "https://lepao-cloud.xxoo365.top/view.php/fcb54dcc5e6209381e972ef73bdb4a93.png";
         }
 
         if (!id) {
             if (countRows.length !== 0) {
-                sql = 'UPDATE lepao_account SET create_user = ?, email = ?, user_avatar = ?, area = ?, auto_time = ?, auto_run = ?, target_count = ?, create_time = ?, update_time = ?, notes = ?, auto_day = ?, notice_type = ? WHERE id = ?'
-                r = await db.query(sql, [uuid, email ?? '', user_avatar ?? '', area, auto_time, auto_run, targetKm, time, time, notes ?? '', JSON.stringify(auto_day), notice_type, countRows[0].id])
+                sql = "UPDATE lepao_account SET create_user = ?, email = ?, user_avatar = ?, area = ?, auto_time = ?, auto_run = ?, target_count = ?, create_time = ?, update_time = ?, notes = ?, auto_day = ?, notice_type = ?, auto_run_distance_min_km = ?, auto_run_distance_max_km = ?, pace_min_sec_per_km = ?, pace_max_sec_per_km = ? WHERE id = ?";
+                r = await db.query(sql, [
+                    uuid,
+                    email ?? "",
+                    user_avatar ?? "",
+                    area,
+                    auto_time,
+                    auto_run,
+                    targetKm,
+                    time,
+                    time,
+                    notes ?? "",
+                    JSON.stringify(auto_day),
+                    notice_type,
+                    preset.autoRunDistanceMinKm,
+                    preset.autoRunDistanceMaxKm,
+                    preset.paceMinSecPerKm,
+                    preset.paceMaxSecPerKm,
+                    countRows[0].id
+                ]);
             }
             else {
                 const bind_code = await this.generateCode()
 
-                sql = 'INSERT INTO lepao_account (student_num, email, user_avatar, area, auto_time, auto_run, target_count, create_user, create_time, notes, auto_day, notice_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
-                r = await db.query(sql, [student_num, email ?? '', user_avatar ?? '', area, auto_time, auto_run, targetKm, uuid, time, notes ?? '', JSON.stringify(auto_day), notice_type])
+                sql = "INSERT INTO lepao_account (student_num, email, user_avatar, area, auto_time, auto_run, target_count, create_user, create_time, notes, auto_day, notice_type, auto_run_distance_min_km, auto_run_distance_max_km, pace_min_sec_per_km, pace_max_sec_per_km) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+                r = await db.query(sql, [
+                    student_num,
+                    email ?? "",
+                    user_avatar ?? "",
+                    area,
+                    auto_time,
+                    auto_run,
+                    targetKm,
+                    uuid,
+                    time,
+                    notes ?? "",
+                    JSON.stringify(auto_day),
+                    notice_type,
+                    preset.autoRunDistanceMinKm,
+                    preset.autoRunDistanceMaxKm,
+                    preset.paceMinSecPerKm,
+                    preset.paceMaxSecPerKm
+                ]);
 
                 let faceSql = 'INSERT INTO lepao_extra (student_num, bind_code) VALUES (?, ?)'
                 let faceRows = await db.query(faceSql, [student_num, bind_code])
@@ -133,8 +279,25 @@ class AddAccount extends API {
                     return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
             }
         } else {
-            sql = 'UPDATE lepao_account SET student_num = ?, email = ?, area = ?, auto_time = ?, target_count = ?, auto_run = ?, notes = ?, auto_day = ?, update_time = ?, notice_type = ? WHERE id = ?'
-            r = await db.query(sql, [student_num, email ?? '', area, auto_time, targetKm, auto_run, notes ?? '', JSON.stringify(auto_day), time, notice_type, id])
+            sql = "UPDATE lepao_account SET student_num = ?, email = ?, area = ?, auto_time = ?, target_count = ?, auto_run = ?, notes = ?, auto_day = ?, update_time = ?, notice_type = ?, auto_run_distance_min_km = ?, auto_run_distance_max_km = ?, pace_min_sec_per_km = ?, pace_max_sec_per_km = ? WHERE id = ? AND create_user = ?";
+            r = await db.query(sql, [
+                student_num,
+                email ?? "",
+                area,
+                auto_time,
+                targetKm,
+                auto_run,
+                notes ?? "",
+                JSON.stringify(auto_day),
+                time,
+                notice_type,
+                preset.autoRunDistanceMinKm,
+                preset.autoRunDistanceMaxKm,
+                preset.paceMinSecPerKm,
+                preset.paceMaxSecPerKm,
+                id,
+                uuid
+            ]);
         }
 
         try {
@@ -159,7 +322,20 @@ class AddAccount extends API {
                     ...BaseStdResponse.OK,
                     id: r.insertId,
                     data: {
-                        student_num, email, id, area, auto_time, auto_run, target_count: targetKm, auto_day, notice_type, notes,
+                        student_num,
+                        email,
+                        id,
+                        area,
+                        auto_time,
+                        auto_run,
+                        target_count: targetKm,
+                        auto_day,
+                        notice_type,
+                        notes,
+                        auto_run_distance_min_km: preset.autoRunDistanceMinKm,
+                        auto_run_distance_max_km: preset.autoRunDistanceMaxKm,
+                        pace_min_sec_per_km: preset.paceMinSecPerKm,
+                        pace_max_sec_per_km: preset.paceMaxSecPerKm,
                         bind_code: selectRows.length !== 0 ? selectRows[0].bind_code : undefined,
                         bot_account: selectRows.length !== 0 ? selectRows[0].bot_account : undefined
                     }

+ 4 - 0
apis/Lepao/Account/Admin/GetAccountList.js

@@ -83,6 +83,10 @@ class GetAccountList extends API {
                     l.homeAddr,
                     l.identifyCode,
                     l.target_count,
+                    l.auto_run_distance_min_km,
+                    l.auto_run_distance_max_km,
+                    l.pace_min_sec_per_km,
+                    l.pace_max_sec_per_km,
                     l.notice_type,
                     f.bind_code,
                     f.bot_account,

+ 4 - 0
apis/Lepao/Account/GetAccount.js

@@ -67,6 +67,10 @@ class GetAccount extends API {
                     l.today_auto_time,
                     l.notes,
                     l.target_count,
+                    l.auto_run_distance_min_km,
+                    l.auto_run_distance_max_km,
+                    l.pace_min_sec_per_km,
+                    l.pace_max_sec_per_km,
                     l.auto_day,
                     l.notice_type,
                     l.userAgent,

+ 10 - 4
lib/Lepao/Worker.js

@@ -352,10 +352,16 @@ class Worker {
                     }
                     pace = p
                 } else {
-                    pace = randomPaceSecPerKm(
-                        jkesSettings.paceRandomMinSecPerKm,
-                        jkesSettings.paceRandomMaxSecPerKm
-                    )
+                    const rMin = Number(req.paceRandomMinSecPerKm)
+                    const rMax = Number(req.paceRandomMaxSecPerKm)
+                    if (Number.isFinite(rMin) && Number.isFinite(rMax)) {
+                        pace = randomPaceSecPerKm(rMin, rMax)
+                    } else {
+                        pace = randomPaceSecPerKm(
+                            jkesSettings.paceRandomMinSecPerKm,
+                            jkesSettings.paceRandomMaxSecPerKm
+                        )
+                    }
                 }
 
                 await this.handlers['lepao.consumeCount'](

+ 191 - 0
plugin/jkes/autoRunAccountOptions.js

@@ -0,0 +1,191 @@
+const { getJkesSettings } = require('./jkesSettings')
+const { MANUAL_PACE_MIN_SEC, MANUAL_PACE_MAX_SEC } = require('./paceUtils')
+
+const DEFAULT_AUTO_RUN_DISTANCE_KM = 2
+
+function roundToStepKm(value, stepKm) {
+    const s = Number(stepKm)
+    const v = Number(value)
+    if (!Number.isFinite(v)) return DEFAULT_AUTO_RUN_DISTANCE_KM
+    if (!Number.isFinite(s) || s <= 0) return Math.round(v * 100) / 100
+    return Math.round(Math.ceil(v / s) * s * 100) / 100
+}
+
+/**
+ * 在 [minKm, maxKm] 内按步进离散均匀随机取一值(用于每次自动乐跑)
+ */
+function randomAutoRunDistanceKmBetween(minKm, maxKm, cfg) {
+    const step = Number(cfg.autoDistanceStepKm) || 0.5
+    const maxCap = Math.max(2, Number(cfg.autoSingleRunMaxKm) || 5)
+    let lo = Number(minKm)
+    let hi = Number(maxKm)
+    if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
+        lo = DEFAULT_AUTO_RUN_DISTANCE_KM
+        hi = DEFAULT_AUTO_RUN_DISTANCE_KM
+    }
+    if (lo > hi) {
+        const t = lo
+        lo = hi
+        hi = t
+    }
+    lo = roundToStepKm(lo, step)
+    hi = roundToStepKm(hi, step)
+    if (lo > hi) {
+        const t = lo
+        lo = hi
+        hi = t
+    }
+    lo = Math.max(1, Math.min(lo, maxCap))
+    hi = Math.max(1, Math.min(hi, maxCap))
+    if (lo > hi) {
+        const t = lo
+        lo = hi
+        hi = t
+    }
+    const span = hi - lo
+    const stepCount = Math.max(0, Math.round(span / step + 1e-9))
+    const pick = Math.floor(Math.random() * (stepCount + 1))
+    const v = lo + pick * step
+    return Math.min(hi, Math.round(v * 100) / 100)
+}
+
+function normalizePaceRangeSecPerKm(rawMin, rawMax, cfg) {
+    const floor = MANUAL_PACE_MIN_SEC
+    const ceil = MANUAL_PACE_MAX_SEC
+    let lo =
+        rawMin === undefined || rawMin === null || rawMin === ''
+            ? Number(cfg.paceRandomMinSecPerKm)
+            : Number(rawMin)
+    let hi =
+        rawMax === undefined || rawMax === null || rawMax === ''
+            ? Number(cfg.paceRandomMaxSecPerKm)
+            : Number(rawMax)
+    if (!Number.isFinite(lo)) lo = floor
+    if (!Number.isFinite(hi)) hi = ceil
+    lo = Math.round(lo)
+    hi = Math.round(hi)
+    if (lo > hi) {
+        const t = lo
+        lo = hi
+        hi = t
+    }
+    lo = Math.max(floor, Math.min(lo, ceil))
+    hi = Math.max(floor, Math.min(hi, ceil))
+    if (lo > hi) {
+        const t = lo
+        lo = hi
+        hi = t
+    }
+    return { paceMinSecPerKm: lo, paceMaxSecPerKm: hi }
+}
+
+/**
+ * 添加/编辑账号:校验自动距离区间与配速区间(body 应已合并库内旧值;兼容仅传 auto_run_distance_km 视为 min=max)
+ * @returns {{ ok: true, autoRunDistanceMinKm: number, autoRunDistanceMaxKm: number, paceMinSecPerKm: number, paceMaxSecPerKm: number } | { ok: false, msg: string }}
+ */
+function validateAutoRunPresetForSave(body) {
+    const cfg = getJkesSettings()
+    const maxKm = Math.max(2, Number(cfg.autoSingleRunMaxKm) || 5)
+    const stepKm = Number(cfg.autoDistanceStepKm) || 0.5
+
+    const legacy = body.auto_run_distance_km
+    const hasMin =
+        body.auto_run_distance_min_km !== undefined &&
+        body.auto_run_distance_min_km !== null &&
+        body.auto_run_distance_min_km !== ''
+    const hasMax =
+        body.auto_run_distance_max_km !== undefined &&
+        body.auto_run_distance_max_km !== null &&
+        body.auto_run_distance_max_km !== ''
+    let rawMin
+    let rawMax
+    if (!hasMin && !hasMax && legacy !== undefined && legacy !== null && legacy !== '') {
+        rawMin = Number(legacy)
+        rawMax = Number(legacy)
+    } else {
+        rawMin = !hasMin ? DEFAULT_AUTO_RUN_DISTANCE_KM : Number(body.auto_run_distance_min_km)
+        rawMax = !hasMax ? DEFAULT_AUTO_RUN_DISTANCE_KM : Number(body.auto_run_distance_max_km)
+        if (hasMin && !hasMax) rawMax = rawMin
+        if (!hasMin && hasMax) rawMin = rawMax
+    }
+    if (!Number.isFinite(rawMin) || !Number.isFinite(rawMax)) {
+        return { ok: false, msg: '自动乐跑距离区间无效' }
+    }
+    if (rawMin < 1 || rawMin > maxKm || rawMax < 1 || rawMax > maxKm) {
+        return {
+            ok: false,
+            msg: `自动乐跑距离区间需在 1–${maxKm} 公里`
+        }
+    }
+    if (rawMin > rawMax) {
+        return { ok: false, msg: '自动乐跑距离下限不能大于上限' }
+    }
+    let autoRunDistanceMinKm = Math.min(maxKm, Math.max(1, roundToStepKm(rawMin, stepKm)))
+    let autoRunDistanceMaxKm = Math.min(maxKm, Math.max(1, roundToStepKm(rawMax, stepKm)))
+    if (autoRunDistanceMinKm > autoRunDistanceMaxKm) {
+        const t = autoRunDistanceMinKm
+        autoRunDistanceMinKm = autoRunDistanceMaxKm
+        autoRunDistanceMaxKm = t
+    }
+
+    const rawLo = body.pace_min_sec_per_km
+    const rawHi = body.pace_max_sec_per_km
+    const lo =
+        rawLo === undefined || rawLo === null || rawLo === ''
+            ? Number(cfg.paceRandomMinSecPerKm)
+            : Number(rawLo)
+    const hi =
+        rawHi === undefined || rawHi === null || rawHi === ''
+            ? Number(cfg.paceRandomMaxSecPerKm)
+            : Number(rawHi)
+    if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
+        return { ok: false, msg: '配速区间无效(请填写每公里秒数,如 300、540)' }
+    }
+    if (
+        lo < MANUAL_PACE_MIN_SEC ||
+        lo > MANUAL_PACE_MAX_SEC ||
+        hi < MANUAL_PACE_MIN_SEC ||
+        hi > MANUAL_PACE_MAX_SEC
+    ) {
+        return { ok: false, msg: '配速区间需在 3:00–10:00/km(180–600 秒/公里)' }
+    }
+    if (lo > hi) {
+        return { ok: false, msg: '配速区间下限不能大于上限' }
+    }
+
+    return {
+        ok: true,
+        autoRunDistanceMinKm,
+        autoRunDistanceMaxKm,
+        paceMinSecPerKm: Math.round(lo),
+        paceMaxSecPerKm: Math.round(hi)
+    }
+}
+
+function buildAutoRunTaskExtrasFromAccountRow(row) {
+    const cfg = getJkesSettings()
+    const targetKm = randomAutoRunDistanceKmBetween(
+        row?.auto_run_distance_min_km,
+        row?.auto_run_distance_max_km,
+        cfg
+    )
+    const { paceMinSecPerKm, paceMaxSecPerKm } = normalizePaceRangeSecPerKm(
+        row?.pace_min_sec_per_km,
+        row?.pace_max_sec_per_km,
+        cfg
+    )
+    return {
+        targetKm,
+        autoDoubleSlot: targetKm >= 2,
+        paceRandomMinSecPerKm: paceMinSecPerKm,
+        paceRandomMaxSecPerKm: paceMaxSecPerKm
+    }
+}
+
+module.exports = {
+    DEFAULT_AUTO_RUN_DISTANCE_KM,
+    validateAutoRunPresetForSave,
+    buildAutoRunTaskExtrasFromAccountRow,
+    randomAutoRunDistanceKmBetween,
+    normalizePaceRangeSecPerKm
+}

+ 3 - 1
plugin/jkes/syncLepaoAccountFromToken.js

@@ -79,7 +79,9 @@ async function syncLepaoAccountFromToken(token, device = {}) {
 
     const findSql = `
         SELECT 
-            a.email, a.create_user, a.auto_run, a.auto_day, a.notice_type, e.bot_umo
+            a.email, a.create_user, a.auto_run, a.auto_day, a.notice_type, a.target_count,
+            a.auto_run_distance_min_km, a.auto_run_distance_max_km, a.pace_min_sec_per_km, a.pace_max_sec_per_km,
+            e.bot_umo
         FROM
             lepao_account a
         LEFT JOIN

+ 6 - 2
plugin/jkes/updateAccountCore.js

@@ -5,6 +5,7 @@ const { BaseStdResponse } = require('../../BaseStdResponse.js')
 const Redis = require('../DataBase/Redis.js')
 const jkesRedisKeys = require('./redisKeys.js')
 const { syncLepaoAccountFromToken } = require('./syncLepaoAccountFromToken.js')
+const { buildAutoRunTaskExtrasFromAccountRow } = require('./autoRunAccountOptions.js')
 
 async function executeLepaoTokenUpdate(ctx, req, res) {
     const { logger, messageQueue } = ctx
@@ -95,9 +96,12 @@ async function executeLepaoTokenUpdate(ctx, req, res) {
             })
                 .then((plan) => {
                     if (plan.run) {
+                        const extras = buildAutoRunTaskExtrasFromAccountRow(findRows[0])
                         enqueueLepaoStartRun(student_num, logger, {
-                            targetKm: plan.targetKm,
-                            autoDoubleSlot: plan.targetKm >= 2
+                            targetKm: extras.targetKm,
+                            autoDoubleSlot: extras.autoDoubleSlot,
+                            paceRandomMinSecPerKm: extras.paceRandomMinSecPerKm,
+                            paceRandomMaxSecPerKm: extras.paceRandomMaxSecPerKm
                         })
                     } else {
                         logger.info(`${student_num} 登录后未触发 JKES 自动乐跑:${plan.reason}`)

+ 4 - 0
plugin/mq/enqueueLepaoStartRun.js

@@ -20,6 +20,10 @@ async function enqueueLepaoStartRun(studentNum, logger, options = {}) {
         if (options.autoDoubleSlot) {
             data.autoDoubleSlot = true
         }
+        if (options.paceRandomMinSecPerKm != null && options.paceRandomMaxSecPerKm != null) {
+            data.paceRandomMinSecPerKm = options.paceRandomMinSecPerKm
+            data.paceRandomMaxSecPerKm = options.paceRandomMaxSecPerKm
+        }
         publishRunforgeTask(channel, {
             id: taskId,
             type: 'lepao.startRun',