<user_query>
Disconnected from the target VM, address: '127.0.0.1:57767', transport: 'socket'

java.lang.IllegalStateException: Failed to load ApplicationContext

	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:123)
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:244)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:227)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:246)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'elevatorLoadBalancer' defined in file [/Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot/baoshi-robot-core/target/classes/com/baoshi/robot/core/ElevatorLoadBalancer.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.baoshi.robot.repository.machineTask.service.IMachineTaskService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:799)
	at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:228)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1361)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1208)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:755)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:402)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:120)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
	... 26 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.baoshi.robot.repository.machineTask.service.IMachineTaskService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1717)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1273)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:886)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:790)
	... 45 more

</user_query>


扫描整个 `com.baoshi.robot.core` 会注册 `ElevatorLoadBalancer` 等依赖仓储层的 Bean，导致上下文无法装配。改为只扫描 `com.baoshi.redis` 并仅 `Import(StationChainQueueBalancer.class)`。

[REDACTED]


[REDACTED]

Write

package com.baoshi.robot.test.support;

import com.baoshi.robot.core.StationChainQueueBalancer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

/**
 * 集成测试启动类：{@code bootstrap.yml} 与 {@link com.baoshi.robot.app.RobotApplication} 相同，从 Nacos 拉取配置；
 * 仅装配 Redis 相关 Bean 与 {@link StationChainQueueBalancer}，不扫描整个 {@code com.baoshi.robot.core}，
 * 否则会创建 {@code ElevatorLoadBalancer} 等依赖 Mapper/远程服务的 Bean 导致上下文失败。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@SpringBootApplication(scanBasePackages = {"com.baoshi.redis"})
@Import(StationChainQueueBalancer.class)
public class RobotIntegrationTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(RobotIntegrationTestApplication.class, args);
    }
}


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-test test-compile -q

Compile baoshi-robot-test sources


[REDACTED]


[REDACTED]

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core compile -q && mvn -pl baoshi-robot-test test-compile -q

Compile core then robot-test module


[REDACTED]

Verify StationChainQueueBalancer.class exists in core output


[REDACTED]

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-test -am test-compile -q

Test-compile robot-test with reactor deps (-am)


## 【执行方案】

已按前面结论改好 **`RobotIntegrationTestApplication`**，避免扫描整个 `com.baoshi.robot.core`，从而**不再创建**依赖 `IMachineTaskService` 的 **`ElevatorLoadBalancer`** 等 Bean；改为只扫 **`com.baoshi.redis`**，并用 **`@Import(StationChainQueueBalancer.class)`** 单独注册待测的队列 Balancer。

```16:18:/Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot/baoshi-robot-test/src/test/java/com/baoshi/robot/test/support/RobotIntegrationTestApplication.java
@SpringBootApplication(scanBasePackages = {"com.baoshi.redis"})
@Import(StationChainQueueBalancer.class)
public class RobotIntegrationTestApplication {
```

**验证**：在本机执行
`mvn -pl baoshi-robot-test -am test-compile`
已通过（注意仅 `-pl baoshi-robot-test` 时可能从本地仓库拿到**旧**的 `baoshi-robot-core`，会报找不到 `StationChainQueueBalancer`；IDE 里若仍编译失败，可先对父工程 **`Rebuild`** 或保证模块依赖指向当前源码）。

在 IDEA 里重新跑一次集成测试；`Failed to load ApplicationContext` / `NoSuchBeanDefinitionException: IMachineTaskService` 应随之消失。若仍报错，把**新的** `Caused by` 栈贴出来即可。

[REDACTED]


<user_query>

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:86)
	at org.junit.Assert.assertTrue(Assert.java:41)
	at org.junit.Assert.assertTrue(Assert.java:52)
	at com.baoshi.robot.test.core.StationChainQueueBalancerTest.setMaxCapacityIncreasesSlotsAndLeaveQueueRemoves(StationChainQueueBalancerTest.java:78)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

</user_query>


分析问题：查看失败测试与 `StationChainQueueBalancer` 的相关逻辑。

[REDACTED]


怀疑 `setQueueMaxCapacity` 写入的值经 RedisTemplate 序列化后，Lua 中 `GET` + `tonumber` 无法解析，导致仍使用默认上限 3。正在确认 Redis 序列化配置。

[REDACTED]

SemanticSearch

Question: Where is RedisTemplate bean configured with serializers?


[REDACTED]

Shell

find ~/.m2/repository/com/alibaba/fastjson -name "fastjson*.jar" 2>/dev/null | head -3

Locate fastjson jar for serializer inspection


[REDACTED]

javap -classpath ~/.m2/repository/com/alibaba/fastjson/1.2.73/fastjson-1.2.73.jar com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer 2>/dev/null | head -40

Inspect GenericFastJsonRedisSerializer bytecode summary


[REDACTED]

javap -classpath ~/.m2/repository/com/alibaba/fastjson/1.2.73/fastjson-1.2.73.jar -c com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer 2>/dev/null

Disassemble serializer to see JSON features


定位根因：全局 `RedisTemplate` 使用 `GenericFastJsonRedisSerializer` 且带 `WriteClassName`，`setQueueMaxCapacity` 写入的 `String.valueOf(5)` 在 Redis 中为 JSON 而非纯数字；Lua 中 `tonumber(redis.call('GET', ...))` 解析失败，回退为默认上限 3，导致第 4 次入队失败。
制定方案：对上限 key 使用 Redis 原生 UTF-8 数字字符串读写（`RedisCallback` + `connection.set/get`），`getQueueMaxCapacity` 在纯文本解析失败时回退到 `opsForValue().get` 以兼容历史 FastJson 值。

[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        // 与入队 Lua 一致：上限值须为 Redis 中可被 tonumber 解析的纯数字 UTF-8 串（不经 FastJson 包装）
        OptionalInt plain = readQueueMaxCapacityPlain(maxKey);
        if (plain.isPresent()) {
            return plain;
        }
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            if (raw instanceof Number) {
                int v = ((Number) raw).intValue();
                return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
            }
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    public void setQueueMaxCapacity(String warehouse, String stationCode, int maxCapacity) {
        assertWarehouseStation(warehouse, stationCode);
        if (maxCapacity < 0) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "maxCapacity 不能为负数");
        }
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    public void setQueueMaxCapacity(String warehouse, String stationCode, int maxCapacity) {
        assertWarehouseStation(warehouse, stationCode);
        if (maxCapacity < 0) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "maxCapacity 不能为负数");
        }
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        byte[] keyBytes = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey),
                "maxKey serialize");
        byte[] valBytes = String.valueOf(maxCapacity).getBytes(StandardCharsets.UTF_8);
        redisTemplate.execute((RedisCallback<Void>) connection -> {
            connection.set(keyBytes, valBytes);
            return null;
        });
    }

    /**
     * 读取上限原始字符串（供 Lua GET / 人工排查）；若非纯数字则 empty，由调用方再走模板反序列化兼容历史值。
     */
    private OptionalInt readQueueMaxCapacityPlain(String maxKey) {
        byte[] rawBytes = redisTemplate.execute((RedisCallback<byte[]>) connection -> {
            byte[] kb = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey), "maxKey serialize");
            return connection.get(kb);
        });
        if (rawBytes == null || rawBytes.length == 0) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(new String(rawBytes, StandardCharsets.UTF_8).trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }


优化 `getQueueMaxCapacity`：在 key 不存在时避免重复 `get`，并修正逻辑。

[REDACTED]

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        byte[] rawBytes = redisTemplate.execute((RedisCallback<byte[]>) connection -> {
            byte[] kb = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey), "maxKey serialize");
            return connection.get(kb);
        });
        if (rawBytes == null) {
            return OptionalInt.empty();
        }
        if (rawBytes.length > 0) {
            try {
                int v = Integer.parseInt(new String(rawBytes, StandardCharsets.UTF_8).trim());
                if (v >= 0) {
                    return OptionalInt.of(v);
                }
                return OptionalInt.empty();
            } catch (NumberFormatException ex) {
                // 非 Lua 可读纯数字（如历史 FastJson 序列化），走模板反序列化
            }
        }
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            if (raw instanceof Number) {
                int v = ((Number) raw).intValue();
                return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
            }
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        // 与入队 Lua 一致：上限值须为 Redis 中可被 tonumber 解析的纯数字 UTF-8 串（不经 FastJson 包装）
        OptionalInt plain = readQueueMaxCapacityPlain(maxKey);
        if (plain.isPresent()) {
            return plain;
        }
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            if (raw instanceof Number) {
                int v = ((Number) raw).intValue();
                return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
            }
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        });
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {

        });
    }

    /**
     * 读取上限原始字符串（供 Lua GET / 人工排查）；若非纯数字则 empty，由调用方再走模板反序列化兼容历史值。
     */
    private OptionalInt readQueueMaxCapacityPlain(String maxKey) {
        byte[] rawBytes = redisTemplate.execute((RedisCallback<byte[]>) connection -> {
            byte[] kb = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey), "maxKey serialize");
            return connection.get(kb);
        });
        if (rawBytes == null || rawBytes.length == 0) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(new String(rawBytes, StandardCharsets.UTF_8).trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile robot-core and robot-test modules


修复 `RedisSerializer<?>` 泛型与 `String` 不匹配的编译错误：

[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        byte[] rawBytes = redisTemplate.execute((RedisCallback<byte[]>) connection -> {
            byte[] kb = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey), "maxKey serialize");
            return connection.get(kb);
        });

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        @SuppressWarnings("unchecked")
        RedisSerializer<String> keySerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
        byte[] rawBytes = redisTemplate.execute((RedisCallback<byte[]>) connection -> {
            byte[] kb = Objects.requireNonNull(keySerializer.serialize(maxKey), "maxKey serialize");
            return connection.get(kb);
        });

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        byte[] keyBytes = Objects.requireNonNull(redisTemplate.getKeySerializer().serialize(maxKey),
                "maxKey serialize");

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        @SuppressWarnings("unchecked")
        RedisSerializer<String> keySerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
        byte[] keyBytes = Objects.requireNonNull(keySerializer.serialize(maxKey), "maxKey serialize");


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Recompile robot-core and robot-test


## 【分析问题】

失败发生在 `setMaxCapacityIncreasesSlotsAndLeaveQueueRemoves` 第 78 行：第 5 次 `enqueue` 变成 `false`。
队列使用 **Lua** 读 `GET maxKey`，再做 **`tonumber(maxStr)`**。而全局 `RedisTemplate` 的值序列化是 **`GenericFastJsonRedisSerializer`（且带 `WriteClassName`）**，原先 `setQueueMaxCapacity` 里 `opsForValue().set(maxKey, String.valueOf(5))` 写入的是 **JSON 文本**，不是纯数字串，**Lua 解析不到数字**，只能退回默认上限 **3**，所以第 4 条起就会被拒。

## 【执行方案】

已在 `StationChainQueueBalancer` 中改为：

1. **`setQueueMaxCapacity`**：用 **`RedisCallback` + `connection.set`** 写入 **UTF-8 纯数字**（与 Lua `tonumber` 一致）。
2. **`getQueueMaxCapacity`**：先用 **原生 `GET` 字节**解析整数；若不是纯数字（例如历史 FastJson 数据），再 **`opsForValue().get`** 走模板反序列化兼容。

修改文件：`baoshi-robot-core/.../StationChainQueueBalancer.java`（`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过）。

请重新跑 `StationChainQueueBalancerTest`；若仍有失败，把完整断言行与 Redis 里该 max key 的 `GET` 原始值发我即可。

[REDACTED]


<user_query>
没有更简单的方式么,这代码太难看了
</user_query>


改用 `StringRedisTemplate` 读写上限 key，去掉 `RedisCallback` 与字节处理。正在确认项目中是否已有该 Bean。

[REDACTED]


确认 Spring Boot 会在存在自定义 `redisTemplate` 时仍注册默认的 `StringRedisTemplate`。我们改为注入 `StringRedisTemplate`，用 `opsForValue().get/set` 替代 `RedisCallback`。

[REDACTED]


发现工作区中的 `StationChainQueueBalancer` 已回到用 `RedisTemplate` 写字符串的版本，会导致 FastJson 与 Lua 不兼容。更简洁的做法：在 `RedisConfig` 中保证存在 `StringRedisTemplate`，排队上限仅用 `StringRedisTemplate` 读写。

[REDACTED]


[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // Redis全局序列化 配置

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /** 纯字符串读写（与 Lua / 命令行 GET 一致），避免与 {@link #redisTemplate} 的 FastJson 值序列化混用 */
    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(redisConnectionFactory);
        return t;
    }

    // Redis全局序列化 配置

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 *
 * @author zouzhiwen
 * @date 2026/05/06
 */
@Service
public class StationChainQueueBalancer {

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * <p>
 * 排队上限（max key）须以纯数字字符串存储，供 Lua {@code tonumber} 解析，故读写上限使用 {@link StringRedisTemplate}，
 * 与默认 {@code RedisTemplate} 的 FastJson 值序列化区分开。
 *
 * @author zouzhiwen
 * @date 2026/05/06
 */
@Service
public class StationChainQueueBalancer {

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

    private final RedisTemplate<String, Object> redisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate,
            StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        String plain = stringRedisTemplate.opsForValue().get(maxKey);
        if (plain != null) {
            OptionalInt direct = parseNonNegativeInt(plain);
            if (direct.isPresent()) {
                return direct;
            }
        } else {
            return OptionalInt.empty();
        }
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        stringRedisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    private static OptionalInt parseNonNegativeInt(String text) {
        try {
            int v = Integer.parseInt(text.trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile robot modules after StringRedisTemplate change


可以，更简单、也更符合 Spring 习惯的做法是：**排队上限这种要给 Lua / 裸 `GET` 读的 key，不要用带 FastJson 的 `RedisTemplate`，改用 `StringRedisTemplate`。**

## 已做的调整

1. **`RedisConfig`**（`common-redis-spring-boot-starter`）
   - 增加 `StringRedisTemplate` Bean（`@ConditionalOnMissingBean`，避免和 Boot 自带重复）。

2. **`StationChainQueueBalancer`**
   - 构造器注入 `StringRedisTemplate`。
   - **`setQueueMaxCapacity`**：`stringRedisTemplate.opsForValue().set(maxKey, ...)` — 入 Redis 就是纯数字字符串。
   - **`getQueueMaxCapacity`**：先用 `stringRedisTemplate.get` + `parseNonNegativeInt`；若不是合法整数（例如以前用 FastJson 写进去的旧值），再退回一次 `redisTemplate.get` 做兼容。

这样没有 `RedisCallback`、没有手写 `byte[]`，读写都是常见的模板 API。类上补了一句注释，说明 **max key 为什么走 `StringRedisTemplate`**。

`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过。若你愿意**放弃历史 FastJson 上限数据**，还可以把 `getQueueMaxCapacity` 里 `redisTemplate` 那段删掉，会更短；需要兼容旧数据就保留现状即可。

[REDACTED]


<user_query>
为什么代码还是这么麻烦,之前的方式写入的是一个带双引号的字符串,lua脚本不能处理成数字么
</user_query>


在 Lua 中解析带双引号的 JSON 字符串（并尽量兼容 Fastjson 的 `{"value":5}` 形式），即可恢复仅用 `RedisTemplate` 的简洁 Java 代码。正在查看当前文件并简化实现。

[REDACTED]


说明：`tonumber('"5"')` 在 Lua 中为 `nil`（引号不是数字的一部分）。在 Lua 中先去掉 JSON 字符串外层引号或匹配 Fastjson 的 `"value":数字`，即可继续用原来的 `RedisTemplate` 读写。正在把解析逻辑放入 Lua，并恢复简洁的 Java 代码。

[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * <p>
 * 排队上限值由 {@code RedisTemplate}（FastJson 序列化）写入时，在 Redis 中可能表现为 {@code "5"} 或含 {@code "value":5} 的 JSON；
 * Lua 侧对该 GET 结果做解析（纯数字 / 去引号 / 从 Fastjson 形态取 value），与 {@link #getQueueMaxCapacity} 使用同一套存储格式。
 *
 * @author zouzhiwen
 * @date 2026/05/06
 */

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * <p>
 * 排队上限（max key）须以纯数字字符串存储，供 Lua {@code tonumber} 解析，故读写上限使用 {@link StringRedisTemplate}，
 * 与默认 {@code RedisTemplate} 的 FastJson 值序列化区分开。
 *
 * @author zouzhiwen
 * @date 2026/05/06
 */

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local function queueMaxFromRedis(raw, dflt)\n"
                    + "  if raw == false or raw == nil then return dflt end\n"
                    + "  local n = tonumber(raw)\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  local inner = string.match(raw, '^\"(.*)\"$')\n"
                    + "  if inner ~= nil then\n"
                    + "    n = tonumber(inner)\n"
                    + "    if n ~= nil and n >= 0 then return n end\n"
                    + "  end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*(%d+)'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*\"(%d+)\"'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  return dflt\n"
                    + "end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = queueMaxFromRedis(maxStr, defaultMax)\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private final RedisTemplate<String, Object> redisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate,
            StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    public OptionalInt getQueueMaxCapacity(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        String plain = stringRedisTemplate.opsForValue().get(maxKey);
        if (plain != null) {
            OptionalInt direct = parseNonNegativeInt(plain);
            if (direct.isPresent()) {
                return direct;
            }
        } else {
            return OptionalInt.empty();
        }
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        stringRedisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    private static OptionalInt parseNonNegativeInt(String text) {
        try {
            int v = Integer.parseInt(text.trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {


[REDACTED]

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // Redis全局序列化 配置

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /** 纯字符串读写（与 Lua / 命令行 GET 一致），避免与 {@link #redisTemplate} 的 FastJson 值序列化混用 */
    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(redisConnectionFactory);
        return t;
    }

    // Redis全局序列化 配置


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Verify compile after Lua + Java simplification


## 原因

Lua 里 `tonumber` 只认**纯数字文本**。Redis 里若是 JSON 字符串 `"5"`，实际内容是三个字符：`"`、`5`、`"`，所以 `tonumber('"5"')`（在 Lua 里字符串内容是带引号的形态）会得到 **`nil`**，脚本只能退回默认上限。
这不是 Java「麻烦」导致的，而是 **Lua 不会自动按 JSON 规则去引号**。

## 做法

可以在 **Lua 里**做一次很简单的解析（顺序）：

1. 直接 `tonumber(raw)` — 兼容以后若有人用 `SET` 写成裸 `5`
2. `^"(.*)"$` 去掉外层双引号再 `tonumber` — 兼容 Fastjson 对 Java `String` 的序列化
3. `"value":%s*(%d+)` / `"value":%s*"(%d+)"` — 兼容带 `@type` 的 `Integer` 形态

Java 侧已恢复为**只依赖一个 `RedisTemplate`**：`get/set` 仍是一行，并**删掉了** `StringRedisTemplate` 的 Bean 与双模板注入。

**权衡**：解析规则集中在 Lua，以后要兼容新格式就要改脚本；若更希望「Redis 里永远是裸数字」，再考虑 `StringRedisTemplate` 或裸 `SET` 会更硬。当前方案是按你的思路：**不增加 Java 复杂度，让 Lua 认 Fastjson 写出来样子。**

[REDACTED]


<user_query>
我还是觉得写的好麻烦,要不就让写和读都用int
</user_query>


将 Java 侧统一为 `Integer` 读写；Lua 仅保留 `tonumber` 与 Fastjson 的 `"value":数字` 提取（不再处理带引号的字符串）。

[REDACTED]


将 `set` 改为写入 `Integer`（自动装箱），`get` 优先按 `Number` 读取；Lua 仅解析裸数字与 Fastjson 的 `"value":<int>`。

[REDACTED]

StrReplace

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * 排队上限在业务侧统一按 {@code int} 读写；序列化由 {@code RedisTemplate}（FastJson）完成，Lua 仅识别整数字面量或 JSON 中的 {@code "value":整数}。

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local function capFromRedis(raw, dflt)\n"
                    + "  if raw == false or raw == nil then return dflt end\n"
                    + "  local n = tonumber(raw)\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*(%d+)'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  return dflt\n"
                    + "end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = capFromRedis(maxStr, defaultMax)\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, maxCapacity);


[REDACTED]

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 *
 * @author zouzhiwen

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * 排队上限在业务侧统一按 {@code int} 读写；值由 {@code RedisTemplate}（FastJson）序列化，Lua 仅识别裸整数或 JSON 中的 {@code "value":整数}。
 *
 * @author zouzhiwen

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local function capFromRedis(raw, dflt)\n"
                    + "  if raw == false or raw == nil then return dflt end\n"
                    + "  local n = tonumber(raw)\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*(%d+)'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  return dflt\n"
                    + "end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = capFromRedis(maxStr, defaultMax)\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, maxCapacity);
    }


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after int-based Redis max capacity


已按「读写都用 int」收紧实现，整体更短、语义也更一致。

## Java

- **`setQueueMaxCapacity`**：`redisTemplate.opsForValue().set(maxKey, maxCapacity)`，直接存 `int`（装箱为 `Integer`），不再 `String.valueOf`。
- **`getQueueMaxCapacity`**：先判断 `raw instanceof Number` 再 `intValue()`，其余再走 `parseInt`（兜底）。

## Lua

全局 `RedisTemplate` 会把 `Integer` 序列化成带 `"value":数字` 的 JSON，所以脚本里保留一个很小的 **`capFromRedis`**：

1. `tonumber(raw)` — 若以后库里出现裸数字串仍能认
2. `string.match(..., '"value":%s*(%d+)')` — 对上当前 Fastjson 的 `Integer` 形态

不再处理「外层双引号包一层字符串」那种历史写法；新写入一律是 **int/Integer**，与你说的一致。

**说明**：若环境里还有**很早以前**用字符串写入的上限 key，需要清一次 key 或让管理接口重写一次上限；否则 Lua 可能仍按默认容量算。编译已通过。

[REDACTED]


<user_query>
lua脚本一定要这么麻烦么
</user_query>


不必。Lua 麻烦是因为值在 Redis 里是 FastJson 的 JSON；若上限 key 存纯数字字符串，Lua 只需 `tonumber(GET)`。我们改为仅对上限 key 使用 `StringRedisTemplate`，Lua 恢复为最短版本。

[REDACTED]


不必。复杂 Lua 是为解析 FastJson；上限 key 若用 `StringRedisTemplate` 存纯数字串（业务仍是 `int`），Lua 可恢复为 `tonumber(GET)`。

[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // Redis全局序列化 配置

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(redisConnectionFactory);
        return t;
    }

    // Redis全局序列化 配置


[REDACTED]

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * 排队上限在业务上为 {@code int}；对应 Redis key 使用 {@link StringRedisTemplate} 存十进制数字串，与全局 FastJson
 * {@link RedisTemplate} 解耦，Lua 中仅需 {@code tonumber(GET)}。

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate,
            StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        String text = stringRedisTemplate.opsForValue().get(maxKey);
        if (text == null) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(text.trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        stringRedisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }


[REDACTED]


[REDACTED]

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * 排队上限业务上为 {@code int}；对应 Redis key 用 {@link StringRedisTemplate} 存十进制数字串（与 FastJson 全局 {@link RedisTemplate} 分开），Lua 里 {@code tonumber(GET)} 即可。

 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 * 排队上限在业务侧统一按 {@code int} 读写；值由 {@code RedisTemplate}（FastJson）序列化，Lua 仅识别裸整数或 JSON 中的 {@code "value":整数}。

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local function capFromRedis(raw, dflt)\n"
                    + "  if raw == false or raw == nil then return dflt end\n"
                    + "  local n = tonumber(raw)\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*(%d+)'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    --- "  return dflt\n"
                    + "end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = capFromRedis(maxStr, defaultMax)\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";


[REDACTED]

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate,
            StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local function capFromRedis(raw, dflt)\n"
                    + "  if raw == false or raw == nil then return dflt end\n"
                    + "  local n = tonumber(raw)\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  n = tonumber(string.match(raw, '\"value\":%s*(%d+)'))\n"
                    + "  if n ~= nil and n >= 0 then return n end\n"
                    + "  return dflt\n"
                    + "end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = capFromRedis(maxStr, defaultMax)\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private final RedisTemplate<String, Object> redisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        String text = stringRedisTemplate.opsForValue().get(maxKey);
        if (text == null) {
            return OptionalInt.empty();
        }
        try {
            int v = Integer.parseInt(text.trim());
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        Object raw = redisTemplate.opsForValue().get(maxKey);
        if (raw == null) {
            return OptionalInt.empty();
        }
        if (raw instanceof Number) {
            int v = ((Number) raw).intValue();
            return v < 0 ? OptionalInt.empty() : OptionalInt.of(v);
        }
        try {
            int v = Integer.parseInt(Objects.toString(raw, "").trim());
            if (v < 0) {
                return OptionalInt.empty();
            }
            return OptionalInt.of(v);
        } catch (NumberFormatException ex) {
            return OptionalInt.empty();
        }
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        stringRedisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        redisTemplate.opsForValue().set(maxKey, maxCapacity);
    }


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after simplifying Lua + StringRedisTemplate for max key


**不必。** Lua 之所以变长，是因为 **GET 拿到的是 FastJson 整段 JSON**；要在脚本里省事，就要在 **Redis 里存 Lua 能直接 `tonumber` 的字符串**。

## 做法（已按这个改好）

| 部分 | 方式 |
|------|------|
| 排队 Set、Lua | 仍用原来的 `RedisTemplate` |
| **仅「排队上限」这个 key** | 用 **`StringRedisTemplate`** 读写：`String.valueOf(maxCapacity)` ↔ `Integer.parseInt` |
| **Lua** | 回到最短逻辑：`GET` → `tonumber` → 无效则用默认 |

代价只有一个：**多注入一个 `StringRedisTemplate`**（已在 `RedisConfig` 里用 `@ConditionalOnMissingBean` 提供），Java 侧上限仍然是 **`int` API**，只是底层对 Redis 用数字字符串存储。

若环境里还有**旧版用 FastJson 写的上限 key**，需要删 key 或走接口重写一次上限，否则旧值不再是纯数字串，读上限会空、Lua 会走默认容量。

[REDACTED]


<user_query>
能否不要改RedisConfig配置类,只在StationChainQueueBalancer中生效
</user_query>


在 `StationChainQueueBalancer` 内基于现有 `RedisTemplate` 的 `ConnectionFactory` 创建专用的 `StringRedisTemplate` 实例，并撤销对 `RedisConfig` 的修改。

[REDACTED]


[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    // Redis全局序列化 配置

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(redisConnectionFactory);
        return t;
    }

    // Redis全局序列化 配置

 * 排队上限业务上为 {@code int}；对应 Redis key 使用本类内部创建的 {@link StringRedisTemplate}（与主 {@link RedisTemplate}
 * 共用连接工厂、仅用于该 key 的纯数字串），不向 Spring 注册额外 Bean，Lua 中 {@code tonumber(GET)} 即可。

* 排队上限业务上为 {@code int}；对应 Redis key 用 {@link StringRedisTemplate} 存十进制数字串（与 FastJson 全局 {@link RedisTemplate} 分开），Lua 里 {@code tonumber(GET)} 即可。

    private final RedisTemplate<String, Object> redisTemplate;
    /** 仅用于排队上限 key 的字符串模板，与 {@link #redisTemplate} 同连接，不全局注册 Bean */
    private final StringRedisTemplate queueMaxStringRedis;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.queueMaxStringRedis = createQueueMaxStringRedis(redisTemplate);
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LUA_ENQUEUE);
        script.setResultType(Long.class);
        this.enqueueScript = script;
    }

    private static StringRedisTemplate createQueueMaxStringRedis(RedisTemplate<String, Object> redisTemplate) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(
                Objects.requireNonNull(redisTemplate.getConnectionFactory(), "redisConnectionFactory"));
        t.afterPropertiesSet();
        return t);
        return t;
    }

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate,
            StringRedisTemplate stringRedisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LUA_ENQUEUE);
        script.setResultType(Long.class);
        this.enqueueScript = script;
    }


[REDACTED]

    private static StringRedisTemplate createQueueMaxStringRedis(RedisTemplate<String, Object> redisTemplate) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(
                Objects.requireNonNull(redisTemplate.getConnectionFactory(), "redisConnectionFactory"));
        t.afterPropertiesSet();
        return t;
    }

    private static StringRedisTemplate createQueueMaxStringRedis(RedisTemplate<String, Object> redisTemplate) {
        StringRedisTemplate t = new StringRedisTemplate();
        t.setConnectionFactory(
                Objects.requireNonNull(redisTemplate.getConnectionFactory(), "redisConnectionFactory"));
        t.afterPropertiesSet();
        return t);
        return t;
    }

String text = queueMaxStringRedis.opsForValue().get(maxKey);

String text = stringRedisTemplate.opsForValue().get(maxKey);


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after local StringRedisTemplate in balancer


可以，已经按你的要求做了。

## 改动说明

1. **`RedisConfig`**：已撤销 `StringRedisTemplate` 的 `@Bean` 及相关 import，与修改前一致。

2. **`StationChainQueueBalancer`**：只保留构造注入 `RedisTemplate<String, Object>`；在构造里用 **`redisTemplate.getConnectionFactory()`** 创建一个 **`StringRedisTemplate`**，调用 `afterPropertiesSet()` 后赋给字段 `queueMaxStringRedis`，仅用于上限 key 的 `get/set`。

这样：

- 不向容器里多注册任何 Bean；
- 和原来的 `RedisTemplate` 共用同一套连接工厂（连接池仍是同一份）；
- Lua 仍可保持最简单的 `tonumber(GET)`。

编译：`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过。

[REDACTED]


<user_query>
为什么这样的StringRedisTemplate写进去的就是数字
</user_query>


核心区别在于 **值用什么序列化器写进 Redis**，不是 Java 里你传的是 `String` 还是 `int`。

## `StringRedisTemplate` 默认在写什么

`StringRedisTemplate` 的 key、value 默认都是 **`StringRedisSerializer`**：
把你传的 `String` 按 **UTF-8 原样**写成 Redis 里的字符串。

例如：

```java
queueMaxStringRedis.opsForValue().set(maxKey, String.valueOf(8));
```

Redis 里这段 value 的字节就是数字字符 **`8` 的 UTF-8**（只有一个字节 `0x38`），**没有** JSON 引号、**没有** Fastjson 的 `@type`、`value` 包裹。

所以 Lua 里 `GET` 得到的是类似 `"8"` 这种**纯数字文本**（在 Lua 里就是长度为 1 的字符串），`tonumber` 能直接转成数。

## 对比：带 FastJson 的 `RedisTemplate<String, Object>`

你们全局的 `RedisTemplate` 用的是 **`GenericFastJsonRedisSerializer`**（还带 `WriteClassName`）。
这时即便 Java 写的是 `Integer` 或 `"5"`，进 Redis 的往往是 **一整段 JSON**，例如：

- 字符串可能被编码成带引号的形式；
- 整数常形如 `{"@type":"java.lang.Integer","value":5}` 之类。

Lua 对整段字符串做 `tonumber(整段)` 会得到 **`nil`**，因为**从头到尾不是合法数字字面量**。

## 小结

- **`StringRedisTemplate`**：value = **明文的十进制数字字符串**（仅字符 `0-9`，必要时还有你们代码里的 `String.valueOf` 生成的形式），所以「写进去就是数字」指的是 **Redis 里存的是给 `tonumber` 能吃掉的形态**。
- **`RedisTemplate` + FastJson**：value = **JSON 二进制/文本**，Lua 要么写复杂解析，要么别用这个模板写要给脚本读的 key。

你们当前在 `StationChainQueueBalancer` 里**单独**建一个 `StringRedisTemplate` 专写上限 key，就是为了和队列里仍用 FastJson 的 `RedisTemplate`**划清界限**，只对「Lua 要 `GET` 再 `tonumber`」的那一个 key 使用裸字符串存储。

[REDACTED]


<user_query>
现在入队还要加个参数为bool值,代表是否忽略排队上限,如果忽略,不用管最大队列数量,直接加入队列
</user_query>


查看当前入队实现与调用方，便于给出准确改法。

[REDACTED]


[REDACTED]

SemanticSearch

Question: Where is StationChainQueueBalancer enqueue called for station chain queue?


当前仓库里 **`StationChainQueueBalancer.enqueue` 只在单测里被调用**，业务侧尚未接入；改法集中在「方法签名 + Lua + 测试」，以后谁调 `enqueue` 再传入新参数即可。

## 思路

- **`ignoreQueueMax == false`**：行为与现在一致（先判重，再按 `SCARD` 与上限比较，满了返回 `0`）。
- **`ignoreQueueMax == true`**：**只做判重**，不做「已满」判断，允许 `SADD`（仍不占满逻辑上的「名额」语义由业务自己把控）。

## 1. Java：`enqueue` 增加布尔参数

推荐：

- `boolean enqueue(String warehouse, String stationCode, String chainCode, boolean ignoreQueueMax)`
- 保留三参重载（若希望少改调用方）：`enqueue(w, s, c)` 内部调用 `enqueue(w, s, c, false)`。

`redisTemplate.execute` 多传一个 **字符串** 给 Lua（不要用 `boolean` 直接当 `ARGV`，统一用 `"1"` / `"0"`）：

```text
ARGV[1] = chainCode
ARGV[2] = defaultMax（与现在一致）
ARGV[3] = ignore： "1" 表示忽略上限，"0" 表示不忽略
```

## 2. Lua：在 `SCARD` 与 `max` 比较处加分支

在「重复则 `-1`」之后、**`SADD` 之前**，只有 **`ARGV[3] ~= "1"`** 时才执行：

```text
if redis.call('SCARD', KEYS[1]) >= max then return 0 end
```

若忽略上限，可**不再依赖** `max` 做容量判断（仍可保留前面的 `GET`/ `tonumber` 计算，或仅在 `ignore ~= "1"` 时才算 `max`，略省一次逻辑，非必须）。

返回值约定可不变：`1` 成功，`0` 满，` -1` 重复。

## 3. 行为与文档

- **判重**：建议两种模式都保留（同链号仍返回 `false` / Lua `-1`），除非产品明确「忽略上限时也允许同号重复」。
- **日志**：`V4Log.debug` 里可带上 `ignoreQueueMax`，便于排查。
- **Javadoc**：写明 `ignoreQueueMax` 语义与返回值。

## 4. 测试

在 `StationChainQueueBalancerTest` 里加用例：例如默认上限 3 已放满 3 个，再 `enqueue(..., true)` 应成功，且 `enqueue(..., false)` 仍失败。

---

我当前是 **Ask 模式**，不能直接改仓库。若要我在项目里改好 `StationChainQueueBalancer`、Lua 和测试，请切换到 **Agent 模式** 再说一声即可。

[REDACTED]


<user_query>
开始执行
</user_query>


正在实现：更新 Lua、`enqueue` 重载与集成测试。

[REDACTED]

StrReplace

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local ignoreCap = ARGV[3]\n"
                    + "if ignoreCap == nil then ignoreCap = \"0\" end\n"
                    + "local max = defaultMax\n"
                    + "if ignoreCap ~= \"1\" then\n"
                    + "  local maxStr = redis.call('GET', KEYS[2])\n"
                    + "  max = tonumber(maxStr)\n"
                    + "  if max == nil or max < 0 then max = defaultMax end\n"
                    + "end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if ignoreCap ~= \"1\" then\n"
                    + "  if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    /**
     * 尝试入队；上限读取 {@link RedisConsts#MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(String, String)}。
     *
     * @return true 本次成功加入排队；false 表示已满或已在排队等（可通过 debug 日志查看 Lua 返回值：1 成功，0 已满，-1 重复）
     */
    public boolean enqueue(String warehouse, String stationCode, String chainCode) {
        assertQueueParams(warehouse, stationCode, chainCode);
        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        List<String> keys = new ArrayList<>(2);
        keys.add(queueKey);
        keys.add(maxKey);
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY));
        boolean ok = result != null && result == 1L;
        V4Log.debug("站点任务链排队入队 warehouse={} station={} chain={} luaReturn={} ok={}",
                warehouse, stationCode, chainCode, result, ok);
        return ok;
    }

    /**
     * 尝试入队；遵守站点排队上限与去重规则。
     *
     * @see #enqueue(String, String, String, boolean)
     */
    public boolean enqueue(String warehouse, String stationCode, String chainCode) {
        return enqueue(warehouse, stationCode, chainCode, false);
    }

    /**
     * 尝试入队；上限读取 {@link RedisConsts#MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(String, String)}（忽略上限时仍会去重）。
     *
     * @param ignoreQueueMax {@code true} 时不校验排队人数上限，仅校验是否已存在于集合
     * @return true 本次成功加入；false 表示已满（未忽略上限时）、或已在队（Lua 返回 -1）等
     */
    public boolean enqueue(String warehouse, String stationCode, String chainCode, boolean ignoreQueueMax) {
        assertQueueParams(warehouse, stationCode, chainCode);
        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        List<String> keys = new ArrayList<>(2);
        keys.add(queueKey);
        keys.add(maxKey);
        String ignoreArg = ignoreQueueMax ? "1" : "0";
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreArg);
        boolean ok = result != null && result == 1L;
        V4Log.debug("站点任务链排队入队 warehouse={} station={} chain={} ignoreQueueMax={} luaReturn={} ok={}",
                warehouse, stationCode, chainCode, ignoreQueueMax, result, ok);
        return ok;
    }

        Assert.assertEquals(5L, balancer.queueSize(WAREHOUSE, STATION));
    }
}

        Assert.assertEquals(5L, balancer.queueSize(WAREHOUSE, STATION));
    }

    @Test
    public void ignoreQueueMaxBypassesCapacityButStillRejectsDuplicate() {
        Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "chain-1"));
        Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "chain-2"));
        Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "chain-3"));
        Assert.assertFalse(balancer.enqueue(WAREHOUSE, STATION, "chain-4"));

        Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "chain-4", true));
        Assert.assertEquals(4L, balancer.queueSize(WAREHOUSE, STATION));

        Assert.assertFalse("忽略上限时仍不应重复入队", balancer.enqueue(WAREHOUSE, STATION, "chain-1", true));
    }
}


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile robot-core and robot-test after enqueue flag


## 【执行方案】已落地

### `StationChainQueueBalancer.java`
- **`enqueue(w, s, chain)`**：改为委托 **`enqueue(w, s, chain, false)`**。
- **新增 `enqueue(w, s, chain, boolean ignoreQueueMax)`**：向 Lua 传入第三个参数 **`"1"` / `"0"`**。
- **Lua**：`ARGV[3] == "1"` 时**不读上限 key、不做 `SCARD >= max`**，仍先做 **`SISMEMBER` 去重**，再 **`SADD`**；否则逻辑与原来一致。
- Debug 日志增加 **`ignoreQueueMax`**。

### `StationChainQueueBalancerTest.java`
- **`ignoreQueueMaxBypassesCapacityButStillRejectsDuplicate`**：默认满 3 后第 4 条失败；**`ignoreQueueMax=true` 可入队第 4 条**；同一链号 **`ignoreQueueMax=true` 仍不能重复入队**。

`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过。业务侧调用四参 `enqueue(..., true)` 即可忽略排队上限。

[REDACTED]


<user_query>
2026-05-07 11:09:49.090  INFO [baoshi-robot,,,,,] 86699 --- [pool-3-thread-1] c.b.mq.spring.boot.cachemq.MessageCache  : null

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:86)
	at org.junit.Assert.assertTrue(Assert.java:41)
	at org.junit.Assert.assertTrue(Assert.java:52)
	at com.baoshi.robot.test.core.StationChainQueueBalancerTest.ignoreQueueMaxBypassesCapacityButStillRejectsDuplicate(StationChainQueueBalancerTest.java:95)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

</user_query>


定位问题：`RedisTemplate` 的脚本参数会经 FastJson 序列化，导致 `ARGV[3]` 在 Lua 中不等于 `"1"`，`ignoreCap` 被当成 `"0"`，容量检查仍会拦截。正在改为对第三个参数传入原始 `byte[]`（与 Spring 对 `byte[]` 原样传递的行为一致）。

[REDACTED]


[REDACTED]

StrReplace

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

    /** Lua ARGV[3]：须原样字节传入，避免 RedisTemplate 值序列化（FastJson）后无法与 {@code "1"} 比较 */
    private static final byte[] LUA_ARG_IGNORE_QUEUE_MAX = new byte[] { (byte) '1' };
    private static final byte[] LUA_ARG_RESPECT_QUEUE_MAX = new byte[] { (byte) '0' };

    private static final String LUA_ENQUEUE =

private static final String LUA_ENQUEUE =

        byte[] ignoreFlag = ignoreQueueMax ? LUA_ARG_IGNORE_QUEUE_MAX : LUA_ARG_RESPECT_QUEUE_MAX;
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode.getBytes(StandardCharsets.UTF_8),
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY).getBytes(StandardCharsets.UTF_8), ignoreFlag);

        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreQueueMax ? "1" : "0");


仅将第三个参数改为 `byte[]`。`chainCode` 与 `defaultMax` 改回字符串，避免与现有 FastJson 序列化的 Set member 不一致。

[REDACTED]

        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreFlag);

        byte[] ignoreFlag = ignoreQueueMax ? LUA_ARG_IGNORE_QUEUE_MAX : LUA_ARG_RESPECT_QUEUE_MAX;
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode.getBytes(StandardCharsets.UTF_8),
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY).getBytes(StandardCharsets.UTF_8), ignoreFlag);

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;


[REDACTED]

        keys.add(queueKey);
        keys.add(maxKey);
        byte[] ignoreFlag = ignoreQueueMax ? LUA_ARG_IGNORE_QUEUE_MAX : LUA_ARG_RESPECT_QUEUE_MAX;
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreFlag);

        keys.add(queueKey);
        keys.add(maxKey);
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreFlag);


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Verify compile after ignoreFlag byte[] fix


## 原因

`RedisTemplate` 执行 Lua 时，**脚本参数会走 `GenericFastJsonRedisSerializer`**。
把 `"1"` / `"0"` 当 `String` 传进去后，进 Redis 的已不是单字节字符 `1`，Lua 里 `ignoreCap == "1"` 恒为假，`ARGV[3]` 被当成 `nil` 后又落成默认 `"0"`，**忽略上限从未生效**，队列仍按 `SCARD >= max` 拒绝，第 95 行 `assertTrue` 失败。

## 修改

Spring Data Redis 对 **`byte[]` 脚本参数会原样下发**（不再二次序列化）。
已增加常量 `LUA_ARG_IGNORE_QUEUE_MAX` / `LUA_ARG_RESPECT_QUEUE_MAX`，在 `enqueue(..., ignoreQueueMax)` 里 **`ARGV[3]` 只传这两个 `byte[]`**；`chainCode`、默认上限仍用原来的 `String`，避免和现有 Set 成员序列化方式不一致。

`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过。请在 IDE 里再跑一遍 `StationChainQueueBalancerTest`。

[REDACTED]


<user_query>
直接这样不行么ignoreQueueMax ? "1" : "0".getBytes(StandardCharsets.UTF_8)
</user_query>


三元运算符需注意优先级：`ignoreQueueMax ? "1" : "0".getBytes(...)` 会类型错误。应写为 `(ignoreQueueMax ? "1" : "0").getBytes(StandardCharsets.UTF_8)`，与 `new byte[]{'1'}` 等价。正在替换实现。

[REDACTED]


发现第 95 行写法有误：`ignoreQueueMax ? "1" : "0".getBytes(...)` 会被解析为 `String` 与 `byte[]` 混用，无法通过编译。应改为对整个字符串调用 `getBytes`：

[REDACTED]

StrReplace

        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY),
                (ignoreQueueMax ? "1" : "0").getBytes(StandardCharsets.UTF_8));

        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), ignoreQueueMax ? "1" : "0".getBytes(StandardCharsets.UTF_8));


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core -q test-compile

Compile baoshi-robot-core after ternary getBytes fix


可以，效果和原来的 `new byte[]{'1'}` 一样，**但必须加括号**：

- **错误**：`ignoreQueueMax ? "1" : "0".getBytes(UTF_8)`
  等价于 `ignoreQueueMax ? "1" : ("0".getBytes(...))`，一边是 `String` 一边是 `byte[]`，类型不合法；即便能编过去，`true` 时传的仍是 **String `"1"`**，又会走上 FastJson，问题照旧。

- **正确**：`(ignoreQueueMax ? "1" : "0").getBytes(StandardCharsets.UTF_8)`
  先得到 `"1"` 或 `"0"`，再转成 **原样字节**，再走脚本参数。

代码里已改成上述正确写法，`test-compile` 已通过。

[REDACTED]


<user_query>
2026-05-07 11:17:49.978  INFO [baoshi-robot,,,,,] 87235 --- [pool-3-thread-1] c.b.mq.spring.boot.cachemq.MessageCache  : null

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:86)
	at org.junit.Assert.assertTrue(Assert.java:41)
	at org.junit.Assert.assertTrue(Assert.java:52)
	at com.baoshi.robot.test.core.StationChainQueueBalancerTest.ignoreQueueMaxBypassesCapacityButStillRejectsDuplicate(StationChainQueueBalancerTest.java:95)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

</user_query>


正在排查：测试仍失败，需查看当前代码与 Spring Redis 脚本参数传递。

[REDACTED]


[REDACTED]


改用两个 Lua 脚本分岔：`ignoreQueueMax` 为 true 时只传队列 key（不做容量检查），避免 `ARGV[3]` 经序列化或 Redis/Lua 对缺省参数的处理导致忽略标志失效。

[REDACTED]


改用两套 Lua：`ignoreQueueMax` 时仅用单 key + 单 ARGV（链号），彻底避免第三个脚本参数在 Spring Redis 传递/序列化中的不确定性。

[REDACTED]

StrReplace

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

    /** 遵守排队上限：ARGV 为 chain、defaultMax；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local maxStr = redis.call('GET', KEYS[2])\n"
                    + "local max = tonumber(maxStr)\n"
                    + "if max == nil or max < 0 then max = defaultMax end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    /** 忽略排队上限：仅去重；KEYS 仅队列；ARGV 仅 chain（避免第三参经 RedisTemplate 序列化不可靠） */
    private static final String LUA_ENQUEUE_IGNORE_MAX =
            "local chain = ARGV[1]\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate queueMaxStringRedis;
    private final DefaultRedisScript<Long> enqueueRespectMaxScript;
    private final DefaultRedisScript<Long> enqueueIgnoreMaxScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.queueMaxStringRedis = createQueueMaxStringRedis(redisTemplate);
        this.enqueueRespectMaxScript = script(LUA_ENQUEUE_RESPECT_MAX);
        this.enqueueIgnoreMaxScript = script(LUA_ENQUEUE_IGNORE_MAX);
    }

    private static DefaultRedisScript<Long> script(String lua) {
        DefaultRedisScript<Long> s = new DefaultRedisScript<>();
        s.setScriptText(lua);
        s.setResultType(Long.class);
        return s;
    }

    private static final String LUA_ENQUEUE =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
                    + "local ignoreCap = ARGV[3]\n"
                    + "if ignoreCap == nil then ignoreCap = \"0\" end\n"
                    + "local max = defaultMax\n"
                    + "if ignoreCap ~= \"1\" then\n"
                    + "  local maxStr = redis.call('GET', KEYS[2])\n"
                    + "  max = tonumber(maxStr)\n"
                    + "  if max == nil or max < 0 then max = defaultMax end\n"
                    + "end\n"
                    + "if redis.call('SISMEMBER', KEYS[1], chain) == 1 then return -1 end\n"
                    + "if ignoreCap ~= \"1\" then\n"
                    + "  if redis.call('SCARD', KEYS[1]) >= max then return 0 end\n"
                    + "end\n"
                    + "redis.call('SADD', KEYS[1], chain)\n"
                    + "return 1\n";

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate queueMaxStringRedis;
    private final DefaultRedisScript<Long> enqueueScript;

    public StationChainQueueBalancer(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.queueMaxStringRedis = createQueueMaxStringRedis(redisTemplate);
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LUA_ENQUEUE);
        script.setResultType(Long.class);
        this.enqueueScript = script;
    }

        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        Long result;
        if (ignoreQueueMax) {
            result = redisTemplate.execute(enqueueIgnoreMaxScript, Collections.singletonList(queueKey), chainCode);
        } else {
            List<String> keys = new ArrayList<>(2);
            keys.add(queueKey);
            keys.add(maxKey);
            result = redisTemplate.execute(enqueueRespectMaxScript, keys, chainCode,
                    String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY));
        }

        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        String maxKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(warehouse, stationCode);
        List<String> keys = new ArrayList<>(2);
        keys.add(queueKey);
        keys.add(maxKey);
        Long result = redisTemplate.execute(enqueueScript, keys, chainCode,
                String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY), (ignoreQueueMax ? "1" : "0").getBytes(StandardCharsets.UTF_8));


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after dual-Lua enqueue split


## 原因说明

在你这边的环境里，**第三个脚本参数 `ARGV[3]` 仍未按预期进到 Lua**（`byte[]` 在部分 Spring Redis / 连接实现路径下仍可能被处理得失真，或 Redis Lua 对缺省 `ARGV` 表现为 `false` 而非 `nil`，导致一直走「遵守上限」分支）。因此 `(ignoreQueueMax ? "1" : "0").getBytes(...)` 仍可能失败。

## 修改说明（已提交到代码）

不再用「一个脚本 + 第三个参数」，改为 **两个 Lua**：

1. **`LUA_ENQUEUE_RESPECT_MAX`**（与原先一致）：`KEYS` = 队列 + 上限，`ARGV` = 链号 + `defaultMax`，做 `GET` 上限、`SCARD` 校验、`SADD`。
2. **`LUA_ENQUEUE_IGNORE_MAX`**：**只做去重 + `SADD`**，`KEYS` **只传队列 key**，`ARGV` **只传链号**，不涉及任何第三参数和上限 key。

Java 里 `ignoreQueueMax ? enqueueIgnoreMaxScript : enqueueRespectMaxScript`，**从根上去掉第三参传递问题**。

已去掉未再使用的 `StandardCharsets` 引用。`mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile` 已通过，请在本地再跑 `ignoreQueueMaxBypassesCapacityButStillRejectsDuplicate`。

[REDACTED]


<user_query>
我现在怀疑DEFAULT_QUEUE_MAX_CAPACITY是否生效
</user_query>


分析 `DEFAULT_QUEUE_MAX_CAPACITY` 的传递路径，并修复 `ARGV[2]` 经 FastJson 序列化导致 `tonumber` 失效、使 Lua 始终走硬编码兜底的问题。

[REDACTED]


确认问题：`ARGV[2]` 传入 `String` 会经 FastJson 序列化，`tonumber(ARGV[2])` 常为 `nil`，Lua 会走 `defaultMax = 3` 的硬编码兜底，导致 Java 的 `DEFAULT_QUEUE_MAX_CAPACITY` 实际上未生效（且与常量不同步）。
修复：1. 将 `ARGV[2]` 改为 `(String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY)).getBytes(StandardCharsets.UTF_8)`；2. 将 Lua 中的兜底改为拼接 `DEFAULT_QUEUE_MAX_CAPACITY`，保持单一数据源。

[REDACTED]

StrReplace

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

    /**
     * Redis 未配置站点单独上限、或上限 key 非法时，Lua 侧使用的排队默认上限。
     * <p>
     * 传入 Lua 的 ARGV[2] 须为原始十进制字节（见 {@link #enqueue(String, String, String, boolean)}），否则经
     * RedisTemplate 的 FastJson 序列化后 {@code tonumber(ARGV[2])} 失效，只会依赖脚本内兜底数字。
     */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 为 chain、defaultMax（十进制 UTF-8 字节）；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = " + DEFAULT_QUEUE_MAX_CAPACITY
                    + " end\n"

    /** Redis 未配置站点单独上限时使用的默认可排队数量 */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 为 chain、defaultMax；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"

            result = redisTemplate.execute(enqueueRespectMaxScript, keys, chainCode,
                    String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY).getBytes(StandardCharsets.UTF_8));

            result = redisTemplate.execute(enqueueRespectMaxScript, keys, chainCode,
                    String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY));


[REDACTED]

    /**
     * Redis 未配置站点单独上限、或上限 key 非法时，Lua 侧使用的排队默认上限。
     * <p>
     * 传入 Lua 的 ARGV[2] 须为原始十进制字节，否则经 RedisTemplate 的 FastJson 序列化后 {@code tonumber(ARGV[2])} 失效，
     * 只会走脚本内兜底分支（与本常量同步拼接）。
     */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 为 chain、defaultMax（十进制 UTF-8 字节）；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = " + DEFAULT_QUEUE_MAX_CAPACITY
                    + " end\n"

    /** Redis 未配置站点上限时使用的默认可排队数量 */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 为 chain、defaultMax；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after DEFAULT_QUEUE_MAX_CAPACITY Lua+ARGV fix


你的怀疑是对的，**之前 `DEFAULT_QUEUE_MAX_CAPACITY` 在 Lua 里基本等于没按 Java 常量生效**。

## 原因

`enqueueRespectMax` 里 **`ARGV[2]` 用的是 `String`**，会走 `RedisTemplate` 的 **FastJson 序列化**。进到 Redis Lua 里往往**不是**纯数字串，`tonumber(ARGV[2])` 经常是 **`nil`**，于是每次都落到脚本里的：

`if defaultMax == nil ... then defaultMax = 3 end`

这里又是**写死的 `3`**，和 Java 里的 `DEFAULT_QUEUE_MAX_CAPACITY` **脱钩**：你把 Java 改成 5，只要 `tonumber` 仍失败，Lua 仍用 3。

## 已做修改

1. **`ARGV[2]`** 改为 **`String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY).getBytes(StandardCharsets.UTF_8)`**，与 `ignore` 开关那次的处理一致，保证 Lua 收到的是**裸十进制字节**，`tonumber` 能稳定解析到 Java 常量。
2. Lua 兜底由 **`= 3`** 改为 **`= ` + `DEFAULT_QUEUE_MAX_CAPACITY`** 拼进脚本，兜底也和 Java **同一常量**（万一 `tonumber` 仍失败，也不会和 Java 不一致）。
3. 给常量补了注释，说明 **`ARGV[2]` 必须原样字节**，否则只能靠脚本兜底。

Java 侧的 `effectiveQueueMaxCapacity` / `getQueueMaxCapacity` 本来就是读上限 key 或默认常量，**不受此问题影响**；受影响的是 **Lua 里 `defaultMax` 这一支**（无配置或 Redis 上限解析失败时用的默认）。

`test-compile` 已通过。

[REDACTED]


<user_query>
拼进脚本后就不需要再传递了
</user_query>


将默认上限完全内联到 Lua 脚本，移除 `ARGV[2]`；`ARGV` 仅保留 `chain`。

[REDACTED]


[REDACTED]

StrReplace

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

    /**
     * Redis 未配置站点单独上限、或上限 key 非法时，Lua 侧使用的排队默认上限（已写入 {@link #LUA_ENQUEUE_RESPECT_MAX}，无需再经 ARGV 传递）。
     */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 仅 chain；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = " + DEFAULT_QUEUE_MAX_CAPACITY + "\n"

    /**
     * Redis 未配置站点单独上限、或上限 key 非法时，Lua 侧使用的排队默认上限。
     * <p>
     * 传入 Lua 的 ARGV[2] 须为原始十进制字节，否则经 RedisTemplate 的 FastJson 序列化后 {@code tonumber(ARGV[2])} 失效，
     * 只会走脚本内兜底分支（与本常量同步拼接）。
     */
    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    /** 遵守排队上限：ARGV 为 chain、defaultMax（十进制 UTF-8 字节）；KEYS 为队列、上限 */
    private static final String LUA_ENQUEUE_RESPECT_MAX =
            "local chain = ARGV[1]\n"
                    + "local defaultMax = tonumber(ARGV[2])\n"
                    + "if defaultMax == nil or defaultMax < 0 then defaultMax = " + DEFAULT_QUEUE_MAX_CAPACITY
                    + " end\n"

result = redisTemplate.execute(enqueueRespectMaxScript, keys, chainCode);

            result = redisTemplate.execute(enqueueRespectMaxScript, keys, chainCode,
                    String.valueOf(DEFAULT_QUEUE_MAX_CAPACITY).getBytes(StandardCharsets.UTF_8));


[REDACTED]

/** 忽略排队上限：仅去重；KEYS 仅队列；ARGV 仅 chain（避免第三参经 RedisTemplate 序列化不可靠） */

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core,baoshi-robot-test -am test-compile -q

Compile after inlining default max in Lua only


已按你的意思改好了。

## 变更说明

- **`LUA_ENQUEUE_RESPECT_MAX`**：`defaultMax` 在脚本里写死为 **`local defaultMax = <Java 常量拼接值>`**，删掉 `tonumber(ARGV[2])` 及兜底分支。
- **`enqueue`**：`execute(enqueueRespectMaxScript, keys, chainCode)`，**不再传**第二参数。
- 去掉未再使用的 **`StandardCharsets`** 引用。
- 注释：`DEFAULT_QUEUE_MAX_CAPACITY` 说明已与脚本内联；忽略上限脚本的注释去掉「第三参」表述。

改 Java 里 `DEFAULT_QUEUE_MAX_CAPACITY` 后需重新编译，脚本里的数字会随常量一起变。`test-compile` 已通过。

[REDACTED]