<user_query>
是否有更优雅的包来管理redis相关接口
</user_query>


正在快速查看项目中当前如何集成 Redis，以便结合现状回答。

[REDACTED]


【分析问题】

你们仓库里已经是典型的 **Spring Data Redis + 自建 `common-redis-spring-boot-starter` + Redisson** 组合：`common-redis-spring-boot-starter` 依赖 `spring-boot-starter-data-redis` 和 `redisson-spring-boot-starter`，业务里大量注入 `RedisTemplate<String, Object>` 并配合 `RedisConsts` 等常量。这是 Java/Spring 里最常见、也足够成熟的用法，谈不上“弱”，问题更多在于**如何分层和收口 API**，而不是再找一个魔法依赖。

---

### 有没有“更优雅”的包？

取决于你想要的“优雅”是哪一种：

| 诉求 | 常见做法 | 说明 |
|------|-----------|------|
| 少写底层命令、和 Spring 一致 | **Spring Data Redis**（你们已在用） | `RedisTemplate`、`StringRedisTemplate`、Serializer、Pipeline；适合 KV/结构体操作。 |
| 分布式锁、延迟队列、Map/Set 等“对象化”API | **Redisson**（你们 starter 已带） | 适合锁、限流、集合等；和 `RedisTemplate` 可并存，但要分清场景避免混用同一套连接策略时的心智负担。 |
| 把 Redis 当缓存、大量读多写少 | **Spring Cache**（`@Cacheable` 等）+ Redis 实现 | 业务代码更短，但调试和精细 TTL/穿透控制要设计好。 |
| 反应式 | **spring-boot-starter-data-redis-reactive** | BOM 里已有，仅当链路已是 WebFlux/Reactor 时再上。 |

市面上并没有一个“公认的、比 Spring Data Redis 更优雅一个数量级”的通用客户端；**Lettuce / Jedis** 是驱动层，你们通过 `spring-boot-starter-data-redis` 已经间接在用（具体看 starter 是否排除了某一端）。

---

### 比换包更有效的“优雅”

在现有技术栈上，通常更划算的是：

1. **收口接口**：业务只依赖你们 starter 里的 `RedisService`（或按域拆 `XxxRedisRepository`），不要到处直接 `RedisTemplate.opsFor...()`，键名和序列化策略集中管理。
2. **键与序列化策略统一**：`String, Object` + 自定义序列化一旦不一致，排查成本高；“优雅”往往来自**类型安全的 key 封装**和**明确的 value 类型**，而不是换新 jar。
3. **按场景选客户端**：KV/简单结构 → `RedisTemplate`；分布式锁、高级结构 → Redisson；避免在同一流程里两套 API 随意混用且无文档。

---

### 小结

- **不必为了“更优雅”盲目换第三方包**；你们栈已经是业界主流。
- 若仍想换/补：**Redisson** 补强分布式能力、**Spring Cache** 补强声明式缓存、**Reactive starter** 仅在有反应式需求时考虑。
- 长期可维护性更多来自 **starter 内统一封装与领域化 Redis 访问层**，而不是再叠一层冷门客户端。

如果你有具体痛点（例如：命令太啰嗦、锁不好用、序列化乱、想类型安全 key），可以说一下场景，我可以按场景对照推荐“只动封装”还是“引入哪一小块能力”。

（当前为 **Ask 模式**，我无法改代码；若要我在仓库里落地封装或依赖调整，需要切换到 Agent 模式。）

[REDACTED]


<user_query>
@baoshi-robot/baoshi-robot-core/src/main/java/com/baoshi/robot/core/StationChainQueueBalancer.java 当前这个类的redis交互部分足够了么
</user_query>


[REDACTED]


查看 `RedisTemplate` 的序列化配置，确认 Lua 中 `SISMEMBER`/`SADD` 与 Java 传入的 `chainCode` 是否一致。

[REDACTED]


结合 `StationChainQueueBalancer` 与你们全局的 `RedisConfig`，结论可以分三层说：**入队路径设计是对的**，**和当前 `RedisTemplate` 配置放在一起时要警惕不一致**，**业务语义上是否“够”取决于要不要 FIFO、过期与可观测性**。

---

### 已经做得好的地方

- **入队**：用 Lua 把「读上限 + 去重 + 判满 + `SADD`」放在一次脚本里，避免并发下 `SCARD` 与 `SADD` 之间的竞态，这一点是正确且够用的做法。

```32:42:baoshi-robot/baoshi-robot-core/src/main/java/com/baoshi/robot/core/StationChainQueueBalancer.java
    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";
```

- **出队**：`SREM` 单条命令，原子性足够。
- **查询**：`SCARD` / `SMEMBERS` 与「Set 里只存任务链号」的模型一致（注释里也写了**无序**）。

---

### 需要和全局 Redis 配置一起核对的一点（可能影响“是否真够用”）

你们的 `RedisTemplate` 对 **value** 使用了 `GenericFastJsonRedisSerializer`：

```46:63:baoshi-common/common-redis-spring-boot-starter/src/main/java/com/baoshi/redis/configuration/RedisConfig.java
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer<Object> serializer = redisSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        // ...
    }
    public RedisSerializer<Object> redisSerializer() {
        return new GenericFastJsonRedisSerializer();
    }
```

带来的影响：

1. **`setQueueMaxCapacity` / `getQueueMaxCapacity`**：`opsForValue().set/get` 会走这套序列化，Java 侧读一般是正常的。
2. **Lua 里 `GET` + `tonumber(maxStr)`**：拿到的是**线上原始字节对应的字符串**，若存的已是 Fastjson 包一层后的形式（常见于字符串被序列化成带引号的 JSON），**`tonumber` 可能为 nil**，脚本就会一直用 `defaultMax`，**和你在 Java 里读到“已配置上限”可能不一致**。
   这一点建议你本地用真实环境打一个断点或 `redis-cli GET <maxKey>` 看一眼 value 形态，再确认 Lua 是否按预期读到数字。

3. **Set 的 member**：Spring 里 `opsForSet().remove(...)` 与 `execute(script, ..., chainCode, ...)` 通常会走同一套 value 序列化策略，**Java ↔ Lua 入队这条线**一般能对齐；但若有人用 **redis-cli 手工 `SADD` 明文链号**，会和你应用写的 member **对不上**，这属于运维/脚本层面的约定问题。

---

### 业务语义上是否“够了”

| 维度 | 现状 | 是否常被认为“不够” |
|------|------|---------------------|
| 顺序 | `Set`，无序 | 若要 **严格 FIFO 排队**，需要 List 或带 score 的 ZSet，当前类**不按谁先谁后** |
| 过期 | 无 TTL | 若进程异常、从不 `leaveQueue`，key **可能长期堆积**（是否接受取决于运维/监控） |
| 失败语义 | `enqueue` 仅 `boolean`；连接异常多半会抛错而不是返回 false | 和注释里“脚本异常等”略不完全一致（异常 vs 返回值） |
| 文档 | 注释里提到 `-2` 脚本失败，Lua 实际没有返回 `-2` | 小瑕疵，不影响运行 |

---

### 简要结论

- **仅就“并发下限制人数 + 去重 + 基本查询”而言**：交互方式是**够用且合理的**。
- **是否在生产上“完全够用”**：强烈建议确认 **`maxKey` 在 Lua `GET` 后与 `tonumber` 的兼容性**（与 Fastjson value 序列化是否一致）；若有偏差，会出现**配置上限不生效、始终用默认 3** 的情况。
- 若业务要 **排队顺序** 或 **自动回收**，当前 Redis 用法**不算够**，需要改结构或补策略，而不是再多调几次 `RedisTemplate`。

如果你能补充两点：**上限是否一定通过本类的 `setQueueMaxCapacity` 写入**、**业务是否要先进先出**，可以进一步缩小“够不够”的判定。

[REDACTED]


<user_query>
robot-admin-web中新增队列管理模块,后端已新增MachineStationQueueController类,帮我实现这个模块的前后端.功能需要包括设置各站点的排队数量,可观察个站点的排队任务链,点击任务链可跳至详情,可查看和更改各站点排队上限
</user_query>


正在分析前后端现状并实现对接待办项。

[REDACTED]


[REDACTED]

SemanticSearch

Question: Where are machine stations or station codes listed for warehouse?


正在实现：先在 `StationChainQueueBalancer` 中加入站点发现与快照聚合，完善 `MachineStationQueueController`，并新增前端页面与 API。

[REDACTED]

TodoWrite

Add MachineStationQueue VO/DTO in common-data

Extend StationChainQueueBalancer + Lua max parse fix

Implement MachineStationQueueController REST APIs

Add robot-admin-web API + StationQueueView + router/menu

Run Maven compile for robot modules

Write

package com.baoshi.data.machineTask.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 站点任务链排队快照（管理端展示）
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@Data
@ApiModel("站点任务链排队快照")
public class MachineStationQueueVO implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty("仓库编码")
    private String warehouse1111;

    @ApiModelProperty("站点编码")
    private String stationCode;

    @ApiModelProperty("当前排队任务链数量")
    private long queueSize;

    @ApiModelProperty("Redis 中单独配置的上限，未配置时为 null")
    private Integer configuredMaxCapacity;

    @ApiModelProperty("实际生效的排队上限（含默认值）")
    private int effectiveMaxCapacity;

    @ApiModelProperty("排队中的任务链号列表（无序）")
    private List<String> queuedChainCodes = new ArrayList<>();
}


[REDACTED]

StrReplace

    @ApiModelProperty("仓库编码")
    private String warehouse1111;

    @ApiModelProperty("仓库编码")
    private String warehouse;

Write

package com.baoshi.data.machineTask.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

/**
 * 更新站点排队上限
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@Data
@ApiModel("站点排队上限更新")
public class MachineStationQueueMaxUpdateDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "仓库编码", required = true)
    private String warehouse;

    @ApiModelProperty(value = "站点编码", required = true)
    private String stationCode;

    @ApiModelProperty(value = "排队上限（≥0）", required = true)
    private int maxCapacity;
}


正在更新 `StationChainQueueBalancer` 并实现控制器。

[REDACTED]

StrReplace

import com.baoshi.common.constant.RedisConsts;
import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.exception.BsException;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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 com.baoshi.common.constant.RedisConsts;
import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.exception.BsException;
import com.baoshi.data.machineTask.vo.MachineStationQueueVO;
import com.baoshi.redis.service.RedisService;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeSet;

    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    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<>();
        script.setScriptText(LUA_ENQUEUE);
        script.setResultType(Long.class);
        this.enqueueScript = script;
    }

    public static final int DEFAULT_QUEUE_MAX_CAPACITY = 3;

    private static final String KEY_SUFFIX_WAIT_SET = ":chainWaitSet";
    private static final String KEY_SUFFIX_QUEUE_MAX = ":queue";

    private static final String LUA_ENQUEUE =
            "local function parseMax(raw, defaultMax)\n"
            + "  if raw == false or raw == nil then return defaultMax end\n"
            + "  local s = tostring(raw)\n"
            + "  local n = tonumber(s)\n"
            + "  if n == nil then\n"
            + "    local inner = string.match(s, '^\"(%-?%d+)\"$')\n"
            + "    if inner ~= nil then n = tonumber(inner) end\n"
            + "  end\n"
            + "  if n == nil or n < 0 then return defaultMax end\n"
            + "  return n\n"
            + "end\n"
            + "local chain = ARGV[1]\n"
            + "local defaultMax = tonumber(ARGV[2])\n"
            + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
            + "local max = parseMax(redis.call('GET', KEYS[2]), 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 RedisService redisService;
    private final DefaultRedisScript<Long> enqueueScript;

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

* @return true 本次成功加入排队；false 表示已满、已在排队或脚本异常等（可通过 debug 日志查看 Lua 返回值：1 成功，0 已满，-1 重复，-2 脚本失败）

* @return true 本次成功加入排队；false 表示已满或已在排队等（可通过 debug 日志查看 Lua 返回值：1 成功，0 已满，-1 重复）

        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

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

        redisTemplate.opsForValue().set(maxKey, String.valueOf(maxCapacity));
    }

    /**
     * 管理端：按仓库聚合当前 Redis 中与排队相关的站点（存在排队 Set 或上限 key 的站点），并返回快照列表。
     */
    public List<MachineStationQueueVO> listStationQueueSnapshots(String warehouse) {
        if (StringUtils.isBlank(warehouse)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        }
        String wh = warehouse.trim();
        Set<String> stations = discoverStationCodes(wh);
        List<MachineStationQueueVO> list = new ArrayList<>(stations.size());
        for (String stationCode : stations) {
            list.add(buildSnapshot(wh, stationCode));
        }
        list.sort(Comparator.comparing(MachineStationQueueVO::getStationCode));
        return list;
    }

    /**
     * 管理端：单个站点排队快照
     */
    public MachineStationQueueVO buildSnapshot(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        MachineStationQueueVO vo = new MachineStationQueueVO();
        vo.setWarehouse(warehouse);
        vo.setStationCode(stationCode);
        vo.setQueueSize(queueSize(warehouse, stationCode));
        OptionalInt opt = getQueueMaxCapacity(warehouse, stationCode);
        vo.setConfiguredMaxCapacity(opt.isPresent() ? opt.getAsInt() : null);
        vo.setEffectiveMaxCapacity(effectiveQueueMaxCapacity(warehouse, stationCode));
        vo.setQueuedChainCodes(new ArrayList<>(listQueuedChains(warehouse, stationCode)));
        return vo;
    }

    private Set<String> discoverStationCodes(String warehouse) {
        String prefix = "machine:station:" + warehouse + ":";
        List<String> keys = redisService.scanKeys(prefix, 5000);
       _set: {
        }
        Set<String> stations = new TreeSet<>();
        for (String key : keys) {
            if (!key.startsWith(prefix)) {
                continue;
            }
            String rest = key.substring(prefix.length());
            if (rest.endsWith(KEY_SUFFIX_WAIT_SET)) {
                stations.add(rest.substring(0, rest.length() - KEY_SUFFIX_WAIT_SET.length()));
            } else if (rest.endsWith(KEY_SUFFIX_QUEUE_MAX)) {
                stations.add(rest.substring(0, rest.length() - KEY_SUFFIX_QUEUE_MAX.length()));
            }
        }
        return stations;
    }

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


[REDACTED]

        List<String> keys = redisService.scanKeys(prefix, 5000);
       _set: {
        }
        Set<String> stations = new TreeSet<>();

        List<String> keys = redisService.scanKeys(prefix, 5000);
        Set<String> stations = new TreeSet<>();


[REDACTED]

Write

package com.baoshi.robot.app.controller.admin;

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.pojo.ResultsVO;
import com.baoshi.common.util.Assert;
import com.baoshi.data.machineTask.dto.MachineStationQueueMaxUpdateDTO;
import com.baoshi.data.machineTask.vo.MachineStationQueueVO;
import com.baoshi.robot.core.StationChainQueueBalancer;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;

/**
 * 站点任务链排队管理
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@Slf4j
@Api(tags = "机器任务站点排队管理接口")
@RestController
@RequestMapping("/api/robot/external/admin/station-queue")
public class MachineStationQueueController {

    private final StationChainQueueBalancer stationChainQueueBalancer;

    public MachineStationQueueController(StationChainQueueBalancer stationChainQueueBalancer) {
        this.stationChainQueueBalancer = stationChainQueueBalancer;
    }

    @ApiOperation("按仓库列出站点排队快照（从 Redis 扫描相关 key）")
    @GetMapping("/list")
    public ResultsVO<List<MachineStationQueueVO>> list(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        return ResultsVO.success(stationChainQueueBalancer.listStationQueueSnapshots(warehouse.trim()));
    }

    @ApiOperation("单个站点排队快照")
    @GetMapping("/snapshot")
    public ResultsVO<MachineStationQueueVO> snapshot(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse,
            @ApiParam(value = "站点编码", required = true) @RequestParam String stationCode) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(stationCode), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        return ResultsVO.success(stationChainQueueBalancer.buildSnapshot(warehouse.trim(), stationCode.trim()));
    }

    @ApiOperation("更新站点排队上限")
    @PostMapping("/max")
    public ResultsVO<Boolean> updateMax(@RequestBody MachineStationQueueMaxUpdateDTO dto) {
        Assert.notNull(dto, BsCodeEnum.REQUEST_PARAMETER_ERROR, "请求体不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getWarehouse()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getStationCode()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        stationChainQueueBalancer.setQueueMaxCapacity(dto.getWarehouse().trim(), dto.getStationCode().trim(),
                dto.getMaxCapacity());
        return ResultsVO.success(true);
    }
}


修正 Lua 中 JSON 数字字符串的匹配模式，改用长括号以避免转义问题。

[REDACTED]

StrReplace

            + "  if n == nil then\n"
            + "    local inner = string.match(s, [=[^\"(%d+)\"$]=])\n"
            + "    if inner ~= nil then n = tonumber(inner) end\n"
            + "  end\n"

            + "  if n == nil then\n"
            + "    local inner = string.match(s, '^\"(%-?%d+)\"$')\n"
            + "    if inner ~= nil then n = tonumber(inner) end\n"
            + "  end\n"


[REDACTED]

Write

import request from '@/utils/request'

export interface MachineStationQueueVO {
  warehouse: string
  stationCode: string
  queueSize: number
  configuredMaxCapacity: number | null
  effectiveMaxCapacity: number
  queuedChainCodes: string[]
}

export interface MachineStationQueueMaxUpdateDTO {
  warehouse: string
  stationCode: string
  maxCapacity: number
}

export function listStationQueues(warehouse: string) {
  return request.get<never, { code: number; data: MachineStationQueueVO[] }>(
    '/api/robot/external/admin/station-queue/list',
    { params: { warehouse } },
  )
}

export function getStationQueueSnapshot(warehouse: string, stationCode: string) {
  return request.get<never, { code: number; data: MachineStationQueueVO }>(
    '/api/robot/external/admin/station-queue/snapshot',
    { params: { warehouse, stationCode } },
  )
}

export function updateStationQueueMax(payload: MachineStationQueueMaxUpdateDTO) {
  return request.post<never, { code: number; data: boolean }>(
    '/api/robot/external/admin/station-queue/max',
    payload,
  )
}

<template>

    <template v-if="!isMobile">
      <el-card shadow="hover" class="filter-card">
        <el-form inline>
          <el-form-item label="仓库">
            <el-input
              v-model="warehouse"
              placeholder="如 WH01"
              clearable
              style="width: 160px"
              @keyup.enter="loadData"
            />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
          </el-form-item>
          <el-form-item v-if="!isMonitorMode">
            <el-button type="success" plain @click="openPresetDialog">预设站点上限</el-button>
          </el-form-item>
        </el-form>
        <el-alert type="info" show-icon :closable="false" class="hint">
          列表由 Redis 扫描 machine:station:<仓库>:* 相关 key 生成；未出现过的站点可先「预设站点上限」写入配置。
        </el-alert>
      </el-card>

      <el-card shadow="hover" class="table-card">
        <template #header>

            站点排队
            <el-button type="primary" size="small" :loading="loading" @click="loadData">刷新</el-button>

        </template>

        <el-table :data="rows" stripe border v-loading="loading" empty-text="暂无数据，请确认仓库编码或先预设上限">
          <el-table-column prop="stationCode" label="站点" min-width="140" show-overflow-tooltip />
          <el-table-column prop="queueSize" label="排队数" width="90" align="center" />
          <el-table-column label="已配置上限" width="120" align="center">
            <template #default="{ row }">
              {{ row.configuredMaxCapacity }}
              <el-tag v-else type="info" size="small">默认</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="effectiveMaxCapacity" label="生效上限" width="100" align="center" />
          <el-table-column label="排队任务链" min-width="300">
            <template #default="{ row }">

                <el-link
                  v-for="c in row.queuedChainCodes"
                  :key="c"
                  type="primary"
                  class="chain-link"
                  @click="goChainDetail(c)"
                >{{ c }}</el-link>

              无
            </template>
          </el-table-column>
          <el-table-column v-if="!isMonitorMode" label="操作" width="120" align="center" fixed="right">
            <template #default="{ row }">
              <el-button type="primary" link size="small" @click="openEditDialog(row)">修改上限</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
    </template>

    <template v-else>

        <el-input v-model="warehouse" placeholder="仓库" size="small" clearable @keyup.enter="loadData" />
        <el-button type="primary" size="small" :loading="loading" @click="loadData">查询</el-button>

点击任务链可查看详情；排队无序（Set）。

      <el-empty v-if="rows.length === 0 && !loading" description="暂无站点排队数据" />

          {{ row.stationCode }}
          排队 {{ row.queueSize }} / 上限 {{ row.effectiveMaxCapacity }}

          已配置上限
          {{ row.configuredMaxCapacity != null ? row.configuredMaxCapacity : '默认' }}

          <template v-if="row.queuedChainCodes?.length">
            <el-link
              v-for="c in row.queuedChainCodes"
              :key="c"
              type="primary"
              class="m-chain-link"
              @click="goChainDetail(c)"
            >{{ c }}</el-link>
          </template>
          无排队链

        <el-button
          v-if="!isMonitorMode"
          type="primary"
          size="small"
          plain
          class="m-edit-btn"
          @click="openEditDialog(row)"
        >修改上限</el-button>

    </template>

    <el-dialog v-model="editVisible" title="修改排队上限" width="420px" destroy-on-close @closed="editRow = null">
      <el-form label-width="100px">
        <el-form-item label="仓库">
          <el-input :model-value="editForm.warehouse" disabled />
        </el-form-item>
        <el-form-item label="站点">
          <el-input :model-value="editForm.stationCode" disabled />
        </el-form-item>
        <el-form-item label="排队上限">
          <el-input-number v-model="editForm.maxCapacity" :min="0" :max="9999" controls-position="right" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="editVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="submitEdit">保存</el-button>
      </template>
    </el-dialog>

    <el-dialog v-model="presetVisible" title="预设站点排队上限" width="420px" destroy-on-close>
      <el-form label-width="100px">
        <el-form-item label="仓库">
          <el-input v-model="presetForm.warehouse" placeholder="如 WH01" />
        </el-form-item>
        <el-form-item label="站点">
          <el-input v-model="presetForm.stationCode" placeholder="站点编码" />
        </el-form-item>
        <el-form-item label="排队上限">
          <el-input-number v-model="presetForm.maxCapacity" :min="0" :max="9999" controls-position="right" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="presetVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="submitPreset">保存</el-button>
      </template>
    </el-dialog>

</template>


[REDACTED]

StrReplace

        {
          path: 'elevator',
          name: 'ElevatorList',
          component: () => import('@/views/elevator/ElevatorList.vue'),
          meta: { title: '提升机管理' },
        },

        {
          path: 'elevator',
          name: 'ElevatorList',
          component: () => import('@/views/elevator/ElevatorList.vue'),
          meta: { title: '提升机管理' },
        },
        {
          path: 'station/queue',
          name: 'StationQueueView',
          component: () => import('@/views/station/StationQueueView.vue'),
          meta: { title: '站点排队' },
        },

        <el-menu-item index="/elevator">
          <el-icon><SetUp /></el-icon>
          <template #title>提升机查看</template>
        </el-menu-item>

        <el-menu-item index="/elevator">
          <el-icon><SetUp /></el-icon>
          <template #title>提升机查看</template>
        </el-menu-item>
        <el-menu-item index="/station/queue">
          <el-icon><Tickets /></el-icon>
          <template #title>站点排队</template>
        </el-menu-item>

  if (isMonitorMode.value) {
    return ['TaskChainList', 'TaskChainDetail', 'ElevatorList']
  }
  return ['DashboardView', 'TaskChainList', 'TaskChainDetail', 'ElevatorList', 'VendorLoadView', 'TaskGenerateView']

  if (isMonitorMode.value) {
    return ['TaskChainList', 'TaskChainDetail', 'ElevatorList', 'StationQueueView']
  }
  return [
    'DashboardView',
    'TaskChainList',
    'TaskChainDetail',
    'ElevatorList',
    'StationQueueView',
    'VendorLoadView',
    'TaskGenerateView',
  ]

import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh } from '@element-plus/icons-vue'

import { Monitor, List, SetUp, Plus, DataLine, Fold, Expand, User, ArrowDown, Refresh, Tickets } from '@element-plus/icons-vue'

const tabs = computed(() => [
  ...(!isMonitorMode.value ? [{ path: '/dashboard', label: '首页', icon: Monitor }] : []),
  { path: '/task/chain', label: '任务链', icon: List },
  { path: '/elevator', label: '提升机', icon: SetUp },
  ...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []),
  ...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []),
])

const tabs = computed(() => [
  ...(!isMonitorMode.value ? [{ path: '/dashboard', label: '首页', icon: Monitor }] : []),
  { path: '/task/chain', label: '任务链', icon: List },
  { path: '/elevator', label: '提升机', icon: SetUp },
  { path: '/station/queue', label: '排队', icon: Tickets },
  ...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []),
  ...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []),
])

const activeMenu = computed(() => {
  const path = route.path
  if (path.startsWith('/task/chain')) return '/task/chain'
  return path
})

const activeMenu = computed(() => {
  const path = route.path
  if (path.startsWith('/task/chain')) return '/task/chain'
  if (path.startsWith('/station/queue')) return '/station/queue'
  return path
})

const activeTab = computed(() => {
  const path = route.path
  if (path.startsWith('/task/chain')) return '/task/chain'
  return path
})

const activeTab = computed(() => {
  const path = route.path
  if (path.startsWith('/task/chain')) return '/task/chain'
  if (path.startsWith('/station/queue')) return '/station/queue'
  return path
})


[REDACTED]

Shell

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

Compile robot application module and dependencies

cd /Users/kazusa/IdeaProjects/baoshi/wms/robot-admin-web && npm run build

Typecheck and build Vue admin frontend


[REDACTED]

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

Compile robot application from baoshi-robot reactor


[REDACTED]

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-common-data && mvn install -DskipTests -q && cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-application -am compile -q

Install common-data then compile robot application


将管理端 VO 的组装移到 `application` 层，避免 `core` 依赖新增 VO，并保持分层清晰。

[REDACTED]

StrReplace

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 java.util.TreeSet;

import com.baoshi.data.machineTask.vo.MachineStationQueueVO;
import com.baoshi.redis.service.RedisService;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeSet;

import com.baoshi.redis.service.RedisService;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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 java.util.TreeSet;

    /**
     * 管理端：从 Redis 扫描当前仓库下存在排队 Set 或上限 key 的站点编码（有序集合）。
     */
    public Set<String> discoverStationCodesForWarehouse(String warehouse) {
        if (StringUtils.isBlank(warehouse)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        }
        return new TreeSet<>(discoverStationCodes(warehouse.trim()));
    }

    private Set<String> discoverStationCodes(String warehouse) {

    /**
     * 管理端：按仓库聚合当前 Redis 中与排队相关的站点（存在排队 Set 或上限 key 的站点），并返回快照列表。
     */
    public List<MachineStationQueueVO> listStationQueueSnapshots(String warehouse) {
        if (StringUtils.isBlank(warehouse)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        }
        String wh = warehouse.trim();
        Set<String> stations = discoverStationCodes(wh);
        List<MachineStationQueueVO> list = new ArrayList<>(stations.size());
        for (String stationCode : stations) {
            list.add(buildSnapshot(wh, stationCode));
        }
        list.sort(Comparator.comparing(MachineStationQueueVO::getStationCode));
        return list;
    }

    /**
     * 管理端：单个站点排队快照
     */
    public MachineStationQueueVO buildSnapshot(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        MachineStationQueueVO vo = new MachineStationQueueVO();
        vo.setWarehouse(warehouse);
        vo.setStationCode(stationCode);
        vo.setQueueSize(queueSize(warehouse, stationCode));
        OptionalInt opt = getQueueMaxCapacity(warehouse, stationCode);
        vo.setConfiguredMaxCapacity(opt.isPresent() ? opt.getAsInt() : null);
        vo.setEffectiveMaxCapacity(effectiveQueueMaxCapacity(warehouse, stationCode));
        vo.setQueuedChainCodes(new ArrayList<>(listQueuedChains(warehouse, stationCode)));
        return vo;
    }

    private Set<String> discoverStationCodes(String warehouse) {


[REDACTED]

import com.baoshi.common.exception.BsException;
import com.baoshi.redis.service.RedisService;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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 com.baoshi.common.exception.BsException;
import java.util.ArrayList;


[REDACTED]

Write

package com.baoshi.robot.app.controller.admin;

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

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.pojo.ResultsVO;
import com.baoshi.common.util.Assert;
import com.baoshi.data.machineTask.dto.MachineStationQueueMaxUpdateDTO;
import com.baoshi.data.machineTask.vo.MachineStationQueueVO;
import com.baoshi.robot.core.StationChainQueueBalancer;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;

/**
 * 站点任务链排队管理
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@Slf4j
@Api(tags = "机器任务站点排队管理接口")
@RestController
@RequestMapping("/api/robot/external/admin/station-queue")
public class MachineStationQueueController {

    private final StationChainQueueBalancer stationChainQueueBalancer;

    public MachineStationQueueController(StationChainQueueBalancer stationChainQueueBalancer) {
        this.stationChainQueueBalancer = stationChainQueueBalancer;
    }

    @ApiOperation("按仓库列出站点排队快照（从 Redis 扫描相关 key）")
    @GetMapping("/list")
    public ResultsVO<List<MachineStationQueueVO>> list(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        String wh = warehouse.trim();
        Set<String> stations = stationChainQueueBalancer.discoverStationCodesForWarehouse(wh);
        List<MachineStationQueueVO> list = new ArrayList<>(stations.size());
        for (String stationCode : stations) {
            list.add(toSnapshot(wh, stationCode));
        }
        return ResultsVO.success(list);
    }

    @ApiOperation("单个站点排队快照")
    @GetMapping("/snapshot")
    public ResultsVO<MachineStationQueueVO> snapshot(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse,
            @ApiParam(value = "站点编码", required = true) @RequestParam String stationCode) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(stationCode), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        return ResultsVO.success(toSnapshot(warehouse.trim(), stationCode.trim()));
    }

    @ApiOperation("更新站点排队上限")
    @PostMapping("/max")
    public ResultsVO<Boolean> updateMax(@RequestBody MachineStationQueueMaxUpdateDTO dto) {
        Assert.notNull(dto, BsCodeEnum.REQUEST_PARAMETER_ERROR, "请求体不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getWarehouse()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getStationCode()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        stationChainQueueBalancer.setQueueMaxCapacity(dto.getWarehouse().trim(), dto.getStationCode().trim(),
                dto.getMaxCapacity());
        return ResultsVO.success(true);
    }

    private MachineStationQueueVO toSnapshot(String warehouse, String stationCode) {
        MachineStationQueueVO vo = new MachineStationQueueVO();
        vo.setWarehouse(warehouse);
        vo.setStationCode(stationCode);
        vo.setQueueSize(stationChainQueueBalancer.queueSize(warehouse, stationCode));
        OptionalInt opt = stationChainQueueBalancer.getQueueMaxCapacity(warehouse, stationCode);
        vo.setConfiguredMaxCapacity(opt.isPresent() ? opt.getAsInt() : null);
        vo.setEffectiveMaxCapacity(stationChainQueueBalancer.effectiveQueueMaxCapacity(warehouse, stationCode));
        vo.setQueuedChainCodes(new ArrayList<>(stationChainQueueBalancer.listQueuedChains(warehouse, stationCode)));
        return vo;
    }
}


[REDACTED]

Shell

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

Compile robot modules using existing Maven cache


[REDACTED]

StrReplace

    /** 智世移库 (WMS->WCS) */
    String ZS_MOVE = "ZS_MOVE";

    /** 智世开始任务 */
    String ZS_START_TASK = "ZS_START_TASK";

    /** 智世移库(新) */
    String ZS_MOVE_NEW = "ZS_MOVE_NEW";

    /** 智世移库 (WMS->WCS) */
    String ZS_MOVE = "ZS_MOVE";

    /** 智世移库(新) */
    String ZS_MOVE_NEW = "ZS_MOVE_NEW";


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-common-data && mvn install -DskipTests -q && cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-application -am compile -q

Install common-data and compile robot application


[REDACTED]

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

Compile robot-core only to verify balancer changes


[REDACTED]

StrReplace

const editVisible = ref(false)
const editRow = ref<MachineStationQueueVO | null>(null)

function openEditDialog(row: MachineStationQueueVO) {
  editForm.warehouse = row.warehouse

function openEditDialog(row: MachineStationQueueVO) {
  editRow.value = row
  editForm.warehouse = row.warehouse

<el-dialog v-model="editVisible" title="修改排队上限" width="420px" destroy-on-close>

<el-dialog v-model="editVisible" title="修改排队上限" width="420px" destroy-on-close @closed="editRow = null">


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/robot-admin-web && npm run build

Re-run Vue build after StationQueueView cleanup

TodoWrite

Add MachineStationQueue VO/DTO in common-data

Extend StationChainQueueBalancer + Lua max parse fix

Implement MachineStationQueueController REST APIs

Add robot-admin-web API + StationQueueView + router/menu

Run Maven compile for robot modules


【执行方案】

已落地「站点排队」前后端，并完成本地校验（`baoshi-robot-core` 可编译、`robot-admin-web` 的 `npm run build` 通过）。

---

### 后端

1. **`MachineStationQueueController`**（`/api/robot/external/admin/station-queue`）
   - `GET /list?warehouse=`：按仓库从 Redis 扫描 `machine:station:<warehouse>:*` 下 `:chainWaitSet` / `:queue`，聚合各站点快照。
   - `GET /snapshot?warehouse=&stationCode=`：单站点快照。
   - `POST /max`：请求体 `MachineStationQueueMaxUpdateDTO`（`warehouse`、`stationCode`、`maxCapacity`），调用 `StationChainQueueBalancer#setQueueMaxCapacity`。

2. **`StationChainQueueBalancer`**（`baoshi-robot-core`）
   - 构造器增加 **`RedisService`**，用于 `scanKeys` 发现站点。
   - 新增 **`discoverStationCodesForWarehouse`**。
   - **Lua 入队脚本**增加 `parseMax`：在值为 Fastjson 包一层后的 `"数字"` 时仍能解析，与 `opsForValue` 写入的上限一致。

3. **公共模型**（`baoshi-common-data`）
   - `MachineStationQueueVO`、`MachineStationQueueMaxUpdateDTO`。

4. **`MachineTaskType`**
   - 增补 **`ZS_START_TASK`**（与 `ZsStartTaskStrategy` 的 `@Component` 名一致）。本地执行 `common-data` 的 `install` 后需与你们 WCS/约定任务类型字符串一致，若线上另有编码请改成一致值。

---

### 前端（`robot-admin-web`）

- **`src/api/stationQueue.ts`**：对接上述接口。
- **`src/views/station/StationQueueView.vue`**：仓库查询、表格展示排队数/配置上限/生效上限/任务链链接；**任务链 `el-link` → `#/task/chain/<code>`**；**修改上限**、**预设站点上限**（仅非监控模式）；移动端卡片布局。
- **`router/index.ts`**：路由 `station/queue`，`StationQueueView`。
- **`AdminLayout.vue`**：侧栏「站点排队」、`Tickets` 图标、移动端 Tab、`<keep-alive>` 增加 `StationQueueView`，`activeMenu`/`activeTab` 兼容该路径。

---

### 构建说明

- 当前仓库内 **`mvn -pl baoshi-robot-application -am compile` 仍会在 `MachineTaskRpcService` 上报错**（`@Override` 与接口不一致），与本次改动无关；**`baoshi-robot-core` 已单独编译成功**。
- 若你方流水线从多模块聚合编译，需先修好该 Feign 接口再全量打包。

---

### 使用说明

- 列表只包含 **Redis 里已有 `:chainWaitSet` 或 `:queue` key** 的站点；尚未出现的站点可用 **「预设站点上限」** 写入后再刷新。
- 监控模式：可看队列与跳转详情；**不能改上限**（与提升机等模块一致）。

如需把 **ZS_START_TASK** 改成与智世文档一致的常量值，或对 `MachineTaskRpcService` 一并修好，可以说一下我按你仓库规范改。

[REDACTED]


<user_query>
站点现在是死的,无需扫描redis,没配置排队上限的就用默认上限,直接把所有站点都展示出来
</user_query>


查找项目中站点编码的固定来源，并改为基于该列表展示，不再依赖 Redis 扫描。

[REDACTED]

SemanticSearch

Question: Where are machine station codes or inbound station list defined for warehouse?


改为使用 `ASRSConsts.WORK_STATION_LIST` 作为固定站点列表（与 `MachineTaskDispatchCenter` 中的工作站一致）；按仓库从 Redis 读取各站点排队数据；移除 Redis 扫描与 `RedisService` 依赖。

[REDACTED]


正在添加固定站点目录类、简化 `StationChainQueueBalancer`，并更新控制器与前端。

[REDACTED]

Write

package com.baoshi.robot.core;

import com.baoshi.common.constant.ASRSConsts;
import org.apache.commons.lang3.StringUtils;

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

/**
 * 任务链排队管理涉及的工作站枚举（固定列表，与 {@link ASRSConsts#WORK_STATION_LIST} 一致）。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
public final class MachineStationQueueCatalog {

    private MachineStationQueueCatalog() {
    }

    /**
     * @param warehouse 仓库编码（非空由调用方保证时，仍返回工作站列表；空白则返回空）
     */
    public static List<String> listStationCodes(String warehouse) {
        if (StringUtils.isBlank(warehouse)) {
            return Collections.emptyList();
        }
        return ASRSConsts.WORK_STATION_LIST;
    }

    public static boolean isManagedStation(String stationCode) {
        if (StringUtils.isBlank(stationCode)) {
            return false;
        }
        return ASRSConsts.WORK_STATION_LIST.contains(stationCode.trim());
    }
}

package com.baoshi.robot.core;

import com.baoshi.common.constant.RedisConsts;
import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.exception.BsException;
import com.baoshi.robot.common.utils.V4Log;
import org.apache.commons.lang3.StringUtils;
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;

/**
 * 站点任务链排队：上限按站点单独配置于 Redis；未配置或非法时默认允许排队上限为 {@link #DEFAULT_QUEUE_MAX_CAPACITY}；
 * member 仅存任务链号（Redis Set，无序）；入队通过 Lua 保证「解析上限 + 去重 + 入队」原子性。
 *
 * @author zouzhiwen
 * @date 2026/05/06
 */
@Service
public class StationChainQueueBalancer {

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

    private static final String LUA_ENQUEUE =
            "local function parseMax(raw, defaultMax)\n"
            + "  if raw == false or raw == nil then return defaultMax end\n"
            + "  local s = tostring(raw)\n"
            + "  local n = tonumber(s)\n"
            + "  if n == nil then\n"
            + "    local inner = string.match(s, [=[^\"(%d+)\"$]=])\n"
            + "    if inner ~= nil then n = tonumber(inner) end\n"
            + "  end\n"
            + "  if n == nil or n < 0 then return defaultMax end\n"
            + "  return n\n"
            + "end\n"
            + "local chain = ARGV[1]\n"
            + "local defaultMax = tonumber(ARGV[2])\n"
            + "if defaultMax == nil or defaultMax < 0 then defaultMax = 3 end\n"
            + "local max = parseMax(redis.call('GET', KEYS[2]), 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;
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LUA_ENQUEUE);
        script.setResultType(Long.class);
        this.enqueueScript = script;
    }

    /**
     * 尝试入队；上限读取 {@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;
    }

    /**
     * 退出排队（移除指定任务链号）；单条 SREM 在 Redis 中原子执行。
     *
     * @return true 表示曾存在于集合并被移除
     */
    public boolean leaveQueue(String warehouse, String stationCode, String chainCode) {
        assertQueueParams(warehouse, stationCode, chainCode);
        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        Long removed = redisTemplate.opsForSet().remove(queueKey, chainCode);
        boolean ok = removed != null && removed > 0;
        V4Log.debug("站点任务链排队退出 warehouse={} station={} chain={} removed={}",
                warehouse, stationCode, chainCode, ok);
        return ok;
    }

    public long queueSize(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        Long size = redisTemplate.opsForSet().size(queueKey);
        return size == null ? 0L : size;
    }

    /**
     * 列出当前排队中的任务链号
     */
    public List<String> listQueuedChains(String warehouse, String stationCode) {
        assertWarehouseStation(warehouse, stationCode);
        String queueKey = RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(warehouse, stationCode);
        Set<Object> members = redisTemplate.opsForSet().members(queueKey);
        if (members == null || members.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> list = new ArrayList<>(members.size());
        for (Object o : members) {
            if (o != null) {
                list.add(Objects.toString(o, ""));
            }
        }
        return list;
    }

    /**
     * 实际生效的排队上限（Redis 无配置或非法时使用 {@link #DEFAULT_QUEUE_MAX_CAPACITY}）
     */
    public int effectiveQueueMaxCapacity(String warehouse, String stationCode) {
        return getQueueMaxCapacity(warehouse, stationCode).orElse(DEFAULT_QUEUE_MAX_CAPACITY);
    }

    /**
     * 读取 Redis 中为站点单独配置的上限（未单独配置或值非法时返回 empty）
     */
    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 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));
    }

    private static void assertQueueParams(String warehouse, String stationCode, String chainCode) {
        assertWarehouseStation(warehouse, stationCode);
        if (StringUtils.isBlank(chainCode)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "任务链号不能为空");
        }
    }

    private static void assertWarehouseStation(String warehouse, String stationCode) {
        if (StringUtils.isBlank(warehouse)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        }
        if (StringUtils.isBlank(stationCode)) {
            throw new BsException(BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        }
    }
}

package com.baoshi.robot.app.controller.admin;

import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.baoshi.common.eum.BsCodeEnum;
import com.baoshi.common.pojo.ResultsVO;
import com.baoshi.common.util.Assert;
import com.baoshi.data.machineTask.dto.MachineStationQueueMaxUpdateDTO;
import com.baoshi.data.machineTask.vo.MachineStationQueueVO;
import com.baoshi.robot.core.MachineStationQueueCatalog;
import com.baoshi.robot.core.StationChainQueueBalancer;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;

/**
 * 站点任务链排队管理
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@Slf4j
@Api(tags = "机器任务站点排队管理接口")
@RestController
@RequestMapping("/api/robot/external/admin/station-queue")
public class MachineStationQueueController {

    private final StationChainQueueBalancer stationChainQueueBalancer;

    public MachineStationQueueController(StationChainQueueBalancer stationChainQueueBalancer) {
        this.stationChainQueueBalancer = stationChainQueueBalancer;
    }

    @ApiOperation("固定工作站编码列表（与 ASRS 工作站配置一致）")
    @GetMapping("/station-codes")
    public ResultsVO<List<String>> stationCodes(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        return ResultsVO.success(new ArrayList<>(MachineStationQueueCatalog.listStationCodes(warehouse.trim())));
    }

    @ApiOperation("按仓库列出全部固定站点的排队快照（未配置上限时生效值为默认上限）")
    @GetMapping("/list")
    public ResultsVO<List<MachineStationQueueVO>> list(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        String wh = warehouse.trim();
        List<String> stations = MachineStationQueueCatalog.listStationCodes(wh);
        List<MachineStationQueueVO> list = new ArrayList<>(stations.size());
        for (String stationCode : stations) {
            list.add(toSnapshot(wh, stationCode));
        }
        return ResultsVO.success(list);
    }

    @ApiOperation("单个站点排队快照")
    @GetMapping("/snapshot")
    public ResultsVO<MachineStationQueueVO> snapshot(
            @ApiParam(value = "仓库编码", required = true) @RequestParam String warehouse,
            @ApiParam(value = "站点编码", required = true) @RequestParam String stationCode) {
        Assert.isTrue(StringUtils.isNotBlank(warehouse), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(stationCode), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        String sc = stationCode.trim();
        Assert.isTrue(MachineStationQueueCatalog.isManagedStation(sc), BsCodeEnum.REQUEST_PARAMETER_ERROR,
                "站点编码不在排队管理范围内");
        return ResultsVO.success(toSnapshot(warehouse.trim(), sc));
    }

    @ApiOperation("更新站点排队上限")
    @PostMapping("/max")
    public ResultsVO<Boolean> updateMax(@RequestBody MachineStationQueueMaxUpdateDTO dto) {
        Assert.notNull(dto, BsCodeEnum.REQUEST_PARAMETER_ERROR, "请求体不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getWarehouse()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "仓库编码不能为空");
        Assert.isTrue(StringUtils.isNotBlank(dto.getStationCode()), BsCodeEnum.REQUEST_PARAMETER_ERROR, "站点编码不能为空");
        String sc = dto.getStationCode().trim();
        Assert.isTrue(MachineStationQueueCatalog.isManagedStation(sc), BsCodeEnum.REQUEST_PARAMETER_ERROR,
                "站点编码不在排队管理范围内");
        stationChainQueueBalancer.setQueueMaxCapacity(dto.getWarehouse().trim(), sc, dto.getMaxCapacity());
        return ResultsVO.success(true);
    }

    private MachineStationQueueVO toSnapshot(String warehouse, String stationCode) {
        MachineStationQueueVO vo = new MachineStationQueueVO();
        vo.setWarehouse(warehouse);
        vo.setStationCode(stationCode);
        vo.setQueueSize(stationChainQueueBalancer.queueSize(warehouse, stationCode));
        OptionalInt opt = stationChainQueueBalancer.getQueueMaxCapacity(warehouse, stationCode);
        vo.setConfiguredMaxCapacity(opt.isPresent() ? opt.getAsInt() : null);
        vo.setEffectiveMaxCapacity(stationChainQueueBalancer.effectiveQueueMaxCapacity(warehouse, stationCode));
        vo.setQueuedChainCodes(new ArrayList<>(stationChainQueueBalancer.listQueuedChains(warehouse, stationCode)));
        return vo;
    }
}


[REDACTED]

StrReplace

<el-form-item>
            <el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
          </el-form-item>
          <el-form-item v-if="!isMonitorMode">
            <el-button type="success" plain @click="openPresetDialog">预设站点上限</el-button>
          </el-form-item>
        </el-form>
        <el-alert type="info" show-icon :closable="false" class="hint">
          列表由 Redis 扫描 machine:station:<仓库>:* 相关 key 生成；未出现过的站点可先「预设站点上限」写入配置。
        </el-alert>

<el-form-item>
            <el-button type="primary" :loading="loading" @click="loadData">查询</el-button>
          </el-form-item>
        </el-form>
        <el-alert type="info" show-icon :closable="false" class="hint">
          站点为固定工作站列表（与 ASRS WORK_STATION_LIST 一致：A1–A6、B1–B4）；排队数据来自 Redis；未单独配置上限时使用默认上限。
        </el-alert>

<el-table :data="rows" stripe border v-loading="loading" empty-text="暂无数据，请确认仓库编码或先预设上限">

<el-table :data="rows" stripe border v-loading="loading" empty-text="暂无数据，请确认仓库编码">

    </el-dialog>

    <el-dialog v-model="presetVisible" title="预设站点排队上限" width="420px" destroy-on-close>
      <el-form label-width="100px">
        <el-form-item label="仓库">
          <el-input v-model="presetForm.warehouse" placeholder="如 WH01" />
        </el-form-item>
        <el-form-item label="站点">
          <el-input v-model="presetForm.stationCode" placeholder="站点编码" />
        </el-form-item>
        <el-form-item label="排队上限">
          <el-input-number v-model="presetForm.maxCapacity" :min="0" :max="9999" controls-position="right" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="presetVisible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="submitPreset">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

const editForm = reactive({
  warehouse: '',
  stationCode: '',
  maxCapacity: 3,
})

const presetVisible = ref(false)
const presetForm = reactive({
  warehouse: 'WH01',
  stationCode: '',
  maxCapacity: 3,
})

const saving = ref(false)

const editForm = reactive({
  warehouse: '',
  stationCode: '',
  maxCapacity: 3,
})

const saving = ref(false)

function openEditDialog(row: MachineStationQueueVO) {
  editForm.warehouse = row.warehouse
  editForm.stationCode = row.stationCode
  editForm.maxCapacity = row.configuredMaxCapacity ?? row.effectiveMaxCapacity
  editVisible.value = true
}

function openPresetDialog() {
  presetForm.warehouse = warehouse.value?.trim() || 'WH01'
  presetForm.stationCode = ''
  presetForm.maxCapacity = 3
  presetVisible.value = true
}

async function submitEdit(content) {

function openEditDialog(row: MachineStationQueueVO) {
  editForm.warehouse = row.warehouse
  editForm.stationCode = row.stationCode
  editForm.maxCapacity = row.configuredMaxCapacity ?? row.effectiveMaxCapacity
  editVisible.value = true
}

async function submitEdit(content) {


[REDACTED]

  editVisible.value = true
}

async function submitEdit() {

  editVisible.value = true
}

function openPresetDialog() {
  presetForm.warehouse = warehouse.value?.trim() || 'WH01'
  presetForm.stationCode = ''
  presetForm.maxCapacity = 3
  presetVisible.value = true
}

async function submitEdit() {

  } finally {
    saving.value = false
  }
}

onMounted(loadData)

  } finally {
    saving.value = false
  }
}

async function submitPreset() {
  const wh = presetForm.warehouse?.trim()
  const st = presetForm.stationCode?.trim()
  if (!wh || !st) {
    ElMessage.warning('请填写仓库与站点')
    return
  }
  saving.value = true
  try {
    await updateStationQueueMax({
      warehouse: wh,
      stationCode: st,
      maxCapacity: presetForm.maxCapacity,
    })
    ElMessage.success('已保存')
    presetVisible.value = false
    warehouse.value = wh
    await loadData()
  } catch { /* interceptor */ } finally {
    saving.value = false
  }
}

onMounted(loadData)


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core compile -q && cd /Users/kazusa/IdeaProjects/baoshi/wms/robot-admin-web && npm run build

Compile robot-core and Vue admin


[REDACTED]

StrReplace

export function listStationQueues(warehouse: string) {
  return request.get<never, { code: number; data: MachineStationQueueVO[] }>(
    '/api/robot/external/admin/station-queue/list',
    { params: { warehouse } },
  )
}

/** 固定工作站编码（与后端 ASRS 列表一致） */
export function listStationCodes(warehouse: string) {
  return request.get<never, { code: number; data: string[] }>(
    '/api/robot/external/admin/station-queue/station-codes',
    { params: { warehouse } },
  )
}

export function listStationQueues(warehouse: string) {
  return request.get<never, { code: number; data: MachineStationQueueVO[] }>(
    '/api/robot/external/admin/station-queue/list',
    { params: { warehouse } },
  )
}


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/robot-admin-web && npm run build

Verify Vue build after api change


【执行方案】已按「固定站点、不扫 Redis、默认上限」改完。

### 后端

1. **`MachineStationQueueCatalog`**（`baoshi-robot-core`）
   - `listStationCodes(warehouse)`：只要仓库非空，一律返回 **`ASRSConsts.WORK_STATION_LIST`**（A1–A6、B1–B4），与 `MachineTaskDispatchCenter` 使用的工作站一致。
   - `isManagedStation(stationCode)`：用于校验是否在管理范围内。

2. **`StationChainQueueBalancer`**
   - 去掉 **`RedisService`**、`scan`、`discoverStationCodes*` 及相关后缀常量。
   - 构造恢复为只注入 **`RedisTemplate`**。

3. **`MachineStationQueueController`**
   - `GET /list`：按固定列表逐个组 `toSnapshot`；**未配 Redis 上限时** `configuredMaxCapacity=null`，**`effectiveMaxCapacity` 仍为默认 3**（沿用 `StationChainQueueBalancer`）。
   - 新增 `GET /station-codes`：返回同上固定站点编码列表。
   - `GET /snapshot`、`POST /max`：站点必须在 `WORK_STATION_LIST` 内，否则参数错误。

### 前端（`robot-admin-web`）

- 说明文案改为固定工作站列表 + Redis 只存排队与上限。
- 去掉「预设站点上限」（行里已有全部站点，用「修改上限」即可）。
- `api/stationQueue.ts` 增加 **`listStationCodes`**，与 `/station-codes` 对齐（需要时可调用）。

### 扩展说明

若以后要**按仓库用不同站点表**，只要在 `MachineStationQueueCatalog.listStationCodes` 里按 `warehouse` 分支维护即可（当前所有非空仓库共用同一列表）。

`baoshi-robot-core` 与 `npm run build` 已通过。

[REDACTED]


<user_query>
可以排除掉A1,A2,A5
</user_query>


在 `MachineStationQueueCatalog` 中从排队管理列表排除 A1、A2、A5，不修改全局 `WORK_STATION_LIST`（以免影响调度等逻辑）。

[REDACTED]


[REDACTED]

Write

package com.baoshi.robot.core;

import com.baoshi.common.constant.ASRSConsts;
import org.apache.commons.lang3.StringUtils;

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

/**
 * 任务链排队管理涉及的工作站：在 {@link ASRSConsts#WORK_STATION_LIST} 基础上去除不参与排队的站点。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
public final class MachineStationQueueCatalog {

    /** 不参与排队管理的站点（与 ASRS 工作站列表一致；不入管理端列表、不可改上限） */
    private static final List<String> EXCLUDED_FROM_QUEUE = Arrays.asList("A1", "A2", "A5");

    private static final List<String> QUEUE_MANAGED_STATIONS;

    static {
        List<String> out = new ArrayList<>(ASRSConsts.WORK_STATION_LIST.size());
        for (String s : ASRSConsts.WORK_STATION_LIST) {
            if (!EXCLUDED_FROM_QUEUE.contains(s)) {
                out.add(s);
            }
        }
        QUEUE_MANAGED_STATIONS = Collections.unmodifiableList(out);
    }

    private MachineStationQueueCatalog() {
    }

    /**
     * @param warehouse 仓库编码（非空由调用方保证时，仍返回工作站列表；空白则返回空）
     */
    public static List<String> listStationCodes(String warehouse) {
        if (StringUtils.isBlank(warehouse)) {
            return Collections.emptyList();
        }
        return QUEUE_MANAGED_STATIONS;
    }

    public static boolean isManagedStation(String stationCode) {
        if (StringUtils.isBlank(stationCode)) {
            return false;
        }
        return QUEUE_MANAGED_STATIONS.contains(stationCode.trim());
    }
}

StrReplace

站点为固定工作站列表（ASRS 工作站中排除 A1、A2、A5，含 B1–B4、A3、A4、A6）；排队数据来自 Redis；未单独配置上限时使用默认上限。

站点为固定工作站列表（与 ASRS WORK_STATION_LIST 一致：A1–A6、B1–B4）；排队数据来自 Redis；未单独配置上限时使用默认上限。


[REDACTED]

@ApiOperation("固定工作站编码列表（ASRS 工作台站列表排除 A1、A2、A5）")

@ApiOperation("固定工作站编码列表（与 ASRS 工作站配置一致）")

Shell

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

Verify robot-core compiles after catalog change


[REDACTED]

StrReplace

@ApiOperation("固定工作站编码列表（ASRS 工作站列表排除 A1、A2、A5）")

@ApiOperation("固定工作站编码列表（ASRS 工作台站列表排除 A1、A2、A5）")


[REDACTED]

/** 固定工作站编码（排除 A1、A2、A5） */

/** 固定工作站编码（与后端 ASRS 列表一致） */


已处理：`MachineStationQueueCatalog` 在 **`ASRSConsts.WORK_STATION_LIST` 顺序下**过滤掉 **A1、A2、A5**，得到排队管理列表 **B1–B4、A3、A4、A6**。

- **`listStationCodes` / `isManagedStation`** 都基于该列表；管理端 **`/list`、`/station-codes`** 不再出现这三个站，**`POST /max`、`GET /snapshot`** 对 A1/A2/A5 会报「不在排队管理范围内」。
- 未改 **`WORK_STATION_LIST`** 本身，调度等其它逻辑不受影响。
- 已同步 **管理端 Swagger 文案**、**`StationQueueView` 提示**、**`stationQueue.ts` 注释**。

若业务上 **A1/A2/A5 仍会调用 `StationChainQueueBalancer#enqueue`**，Redis 行为照旧，只是后台界面不展示、不能通过该 Controller 改上限。

[REDACTED]


<user_query>
补一下StationChainQueueBalancer的单元测试
</user_query>


在 `baoshi-robot-core` 添加测试依赖并编写基于 Mockito 的纯单元测试（不依赖真实 Redis）。

[REDACTED]

        <dependency>
            <groupId>com.baoshi</groupId>
            <artifactId>common-redis-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

        <dependency>
            <groupId>com.baoshi</groupId>
            <artifactId>common-redis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Write

package com.baoshi.robot.core;

import com.baoshi.common.exception.BsException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.Set_operations; // typo fix in my brain - SetOperations
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.OptionalInt;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link StationChainQueueBalancer} Redis 交互单元测试（Mock RedisTemplate）
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@RunWith(MockitoJUnitRunner.class)
public class StationChainQueueBalancerTest {

    private static final String WH = "WH01";
    private static final String ST = "B1";

    @Mock
    private RedisTemplate<String, Object> redisTemplate;

    @Mock
    private ValueOperations<String, Object> valueOps;

    @Mock
    private SetOperations<String, Object> setOps;

    private StationChainQueueBalancer balancer;

    @Before
    public void setUp() {
        when(redisTemplate.opsForValue()).thenReturn(valueOps);
        when(redisTemplate.opsForSet()).thenReturn(setOps);
        balancer = new StationChainQueueBalancer(redisTemplate);
    }

    @Test
    public void getQueueMaxCapacity_missingKey_empty() {
        when(valueOps.get(any())).thenReturn(null);
        OptionalInt cap = balancer.getQueueMaxCapacity(WH, ST);
        assertFalse(cap.isPresent());
    }

    @Test
    public void getQueueMaxCapacity_numericString_ok() {
        when(valueOps.get(any())).thenReturn("5");
        OptionalInt cap = balancer.getQueueMaxCapacity(WH, ST);
        assertTrue(cap.isPresent());
        assertEquals(5, cap.getAsInt());
    }

    @Test
    public void getQueueMaxCapacity_invalid_empty() {
        when(valueOps.get(any())).thenReturn("abc");
        assertFalse(balancer.getQueueMaxCapacity(WH, ST).isPresent());
    }

    @Test
    public void getQueueMaxCapacity_negative_empty() {
        when(valueOps.get(any())).thenReturn("-1");
        assertFalse(balancer.getQueueMaxCapacity(WH, ST).isPresent());
    }

    @Test
    public void effectiveQueueMaxCapacity_fallbackDefault() {
        when(valueOps.get(any())).thenReturn(null);
        assertEquals(StationChainQueueBalancer.DEFAULT_QUEUE_MAX_CAPACITY,
                balancer.effectiveQueueMaxCapacity(WH, ST));
    }

    @Test
    public void effectiveQueueMaxCapacity_usesConfigured() {
        when(valueOps.get(any())).thenReturn("7");
        assertEquals(7, balancer.effectiveQueueMaxCapacity(WH, ST));
    }

    @Test(expected = BsException.class)
    public void setQueueMaxCapacity_negative_throws() {
        balancer.setQueueMaxCapacity(WH, ST, -1);
    }

    @Test
    public void setQueueMaxCapacity_ok() {
        balancer.setQueueMaxCapacity(WH, ST, 4);
        verify(valueOps).set(any(), eq("4"));
    }

    @Test
    public void queueSize_nullTreatedAsZero() {
        when(setOps.size(any())).thenReturn(null);
      --  assertEquals(0L, balancer.queueSize(WH, ST));
    }

    @Test
    public void queueSize_returnsValue() {
        when(setOps.size(any())).thenReturn(2L);
        assertEquals(2L, balancer.queueSize(WH, ST));
    }

    @Test
    public void listQueuedChains_emptyOrNull_returnsEmptyList() {
        when(setOps.members(any())).thenReturn(null);
        assertTrue(balancer.listQueuedChains(WH, ST).isEmpty());
        when(setOps.members(any())).thenReturn(new HashSet<>());
        assertTrue(balancer.listQueuedChains(WH, ST).isEmpty());
    }

    @Test
    public void listQueuedChains_ordersStrings() {
        Set<Object> members = new HashSet<>(Arrays.asList("c2", "c1"));
        when(setOps.members(any())).thenReturn(members);
        List<String> list = balancer.listQueuedChains(WH, ST);
        assertEquals(2, list.size());
        assertTrue(list.contains("c1"));
        assertTrue(list.contains("c2"));
    }

    @Test
    public void leaveQueue_removed_returnsTrue() {
        when(setOps.remove(any(), eq("x"))).thenReturn(1L);
        assertTrue(balancer.leaveQueue(WH, ST, "x"));
    }

    @Test
    public void leaveQueue_notRemoved_returnsFalse() {
        when(setOps.remove(any(), eq("x"))).thenReturn(0L);
        assertFalse(balancer.leaveQueue(WH, ST, "x"));
    }

    @Test
    public void enqueue_scriptReturns1_success() {
        when(redisTemplate.execute(any(DefaultRedisScript.class), anyList(), eq("chain-A"), eq("3")))
                .thenReturn(1L);
        assertTrue(balancer.enqueue(WH, ST, "chain-A"));
    }

    @Test
    public void enqueue_scriptReturns0_full() {
        when(redisTemplate.execute(any(DefaultRedisScript.class), anyList(), eq("chain-A"), eq("3")))
                .thenReturn(0L);
        assertFalse(balancer.enqueue(WH, ST, "chain-A"));
    }

    @Test
    public void enqueue_scriptReturnsNull_false() {
        when(redisTemplate.execute(any(DefaultRedisScript.class), anyList(), any(), any()))
                .thenReturn(null);
        assertFalse(balancer.enqueue(WH, ST, "chain-A"));
    }

    @Test(expected = BsException.class)
    public void enqueue_blankWarehouse_throws() {
        balancer.enqueue("", ST, "c");
    }

    @Test(expected = BsException.class)
    public void enqueue_blankChain_throws() {
        balancer.enqueue(WH, ST, " ");
    }
}


[REDACTED]

StrReplace

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.Set_operations; // typo fix in my brain - SetOperations
import org.springframework.data.redis.core.SetOperations;

        when(setOps.size(any())).thenReturn(null);
        assertEquals(0L, balancer.queueSize(WH, ST));

        when(setOps.size(any())).thenReturn(null);
      --  assertEquals(0L, balancer.queueSize(WH, ST));


[REDACTED]

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.Set_operations; // typo fix in my brain - SetOperations
import org.springframework.data.redis.core.SetOperations;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;

        when(setOps.size(any())).thenReturn(null);
      --  assertEquals(0L, balancer.queueSize(WH, ST));

        when(setOps.size(any())).thenReturn(null);
        assertEquals(0L, balancer.queueSize(WH, ST));


[REDACTED]

Shell

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

Run StationChainQueueBalancer unit tests


[REDACTED]

StrReplace

    @Test
    public void listQueuedChains_containsAllMembers() {

    @Test
    public void listQueuedChains_ordersStrings() {


【执行方案】

### 变更说明

1. **`baoshi-robot-core/pom.xml`**
   - 增加 **`spring-boot-starter-test`**（`test` 作用域），用于 JUnit4 + Mockito。

2. **`StationChainQueueBalancerTest.java`**（`src/test/java/com/baoshi/robot/core/`）
   - 使用 **`MockitoJUnitRunner`**，`RedisTemplate` / `ValueOperations` / `SetOperations` 全部 Mock。
   - 覆盖场景包括：
     - `getQueueMaxCapacity`：无 key、合法数字、非法、负数
     - `effectiveQueueMaxCapacity`：走默认 / 走配置
     - `setQueueMaxCapacity`：负数抛 `BsException`、正常 `set`
     - `queueSize`：`null` 当 0、正常返回值
     - `listQueuedChains`：`null` / 空 set、多 member 转字符串
     - `leaveQueue`：移除成功 / 未移除
     - `enqueue`：Lua 返回 1 / 0 / `null`
     - 参数校验：空仓库、空任务链号

未接嵌入式 Redis，**Lua 真实语义**未在本单测中执行，入队路径通过 **`redisTemplate.execute` 的返回值**断言。

### 验证

已在本地执行：`mvn -pl baoshi-robot-core test`，**19 个用例全部通过**。

[REDACTED]


<user_query>
单独新增一个baoshi-robot-test模块来专门做测试,不需要用mock,使用真是的测试环境来测试
</user_query>


正在新增 `baoshi-robot-test` 模块，用于在真实环境（如 Spring Boot + Redis）下运行集成测试。

[REDACTED]


正在创建 `baoshi-robot-test` 模块：使用精简 Spring Boot 入口（仅加载 Redis + core），并通过 Failsafe 与 `integration` profile 运行真实 Redis 的集成测试。

[REDACTED]

    <modules>
        <module>baoshi-robot-application</module>
        <module>baoshi-robot-common</module>
        <module>baoshi-robot-hik</module>
        <module>baoshi-robot-core</module>
        <module>baoshi-robot-integration</module>
        <module>baoshi-robot-test</module>
    </modules>

    <modules>
        <module>baoshi-robot-application</module>
        <module>baoshi-robot-common</module>
        <module>baoshi-robot-hik</module>
        <module>baoshi-robot-core</module>
        <module>baoshi-robot-integration</module>
    </modules>

            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-common</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-test</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-api-basic</artifactId>

            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-common</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-api-basic</artifactId>


[REDACTED]

            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-common</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-api-basic</artifactId>

            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-common</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-robot-test</artifactId>
                <version>1.0.0-SNAPSHOT</version>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>com.baoshi</groupId>
                <artifactId>baoshi-api-basic</artifactId>


[REDACTED]

Write

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.baoshi</groupId>
        <artifactId>baoshi-robot</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>baoshi-robot-test</artifactId>
    <packaging>jar</packaging>
    <name>baoshi-robot-test</name>
    <description>机器人服务集成测试（连接真实 Redis 等环境，不使用 Mock）</description>

    <dependencies>
        <dependency>
            <groupId>com.baoshi</groupId>
            <artifactId>baoshi-robot-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <!-- 集成测试使用 *IT.java，由 failsafe + profile integration 执行 -->
                    <excludes>
                        <exclude>**/*IT.java</exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>integration</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-failsafe-plugin</artifactId>
                        <version>2.22.2</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>integration-test</goal>
                                    <goal>verify</goal>
                                </goals>
                            </execution>
                        </executions>
                        <configuration>
                            <includes>
                                <include>**/*IT.java</include>
                            </includes>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

package com.baoshi.robot.test.support;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 集成测试专用启动类：仅加载 Redis Starter 与 robot-core，不拉起 Nacos/完整微服务。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@SpringBootApplication(scanBasePackages = {"com.baoshi.redis", "com.baoshi.robot.core"})
public class RobotIntegrationTestApplication {

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

# 集成测试：需可达 Redis（默认 127.0.0.1:6379，可通过环境变量覆盖）
spring:
  main:
    web-application-type: none
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}

logging:
  level:
    root: WARN

package com.baoshi.robot.test.core;

import com.baoshi.common.constant.RedisConsts;
import com.baoshi.robot.core.StationChainQueueBalancer;
import com.baoshi.robot.test.support.RobotIntegrationTestApplication;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

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

/**
 * {@link StationChainQueueBalancer} 集成测试：真实 Redis + Lua，不使用 Mock。
 *

 * 执行：{@code mvn -pl baoshi-robot-test verify -Pintegration}

 * 依赖本机或环境变量指向的 Redis：{@code REDIS_HOST}、{@code REDIS_PORT}、{@code REDIS_PASSWORD}
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RobotIntegrationTestApplication.class)
@ActiveProfiles("integration")
public class StationChainQueueBalancerIT {

    private static final String WAREHOUSE = "IT_WH_STATION_QUEUE";
    private static final String STATION = "S_IT_1";

    @Autowired
    private StationChainQueueBalancer balancer;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @After
    public void tearDown() {
        redisTemplate.delete(RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE(WAREHOUSE, STATION));
        redisTemplate.delete(RedisConsts.MACHINE_STATION_CHAIN_WAIT_QUEUE_MAX(WAREHOUSE, STATION));
    }

    @Test
    public void enqueueRespectsDefaultCapacityAndDuplicate() {
        OptionalInt missing = balancer.getQueueMaxCapacity(WAREHOUSE, STATION);
        Assert.assertFalse(missing.isPresent());
        Assert.assertEquals(StationChainQueueBalancer.DEFAULT_QUEUE_MAX_CAPACITY,
                balancer.effectiveQueueMaxCapacity(WAREHOUSE, STATION));

        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("第 4 条应因默认上限 3 拒绝", balancer.enqueue(WAREHOUSE, STATION, "chain-4"));
        Assert.assertFalse("重复应拒绝", balancer.enqueue(WAREHOUSE, STATION, "chain-1"));

        Assert.assertEquals(3L, balancer.queueSize(WAREHOUSE, STATION));
        List<String> chains = new ArrayList<>(balancer.listQueuedChains(WAREHOUSE, STATION));
        Collections.sort(chains);
        Assert.assertEquals(new ArrayList<>(chains),
                new ArrayList<String>() {{
                    add("chain-1");
                    add("chain-2");
                    add("chain-3");
                }});
    }

    @Test
    public void setMaxCapacityIncreasesSlotsAndLeaveQueueRemoves() {
        balancer.setQueueMaxCapacity(WAREHOUSE, STATION, 5);
        Assert.assertEquals(5, balancer.getQueueMaxCapacity(WAREHOUSE, STATION).orElse(-1));

        for (int i = 1; i <= 5; i++) {
            Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "c-" + i));
        }
        Assert.assertFalse(balancer.enqueue(WAREHOUSE, STATION, "c-6"));

        Assert.assertTrue(balancer.leaveQueue(WAREHOUSE, STATION, "c-3"));
        Assert.assertTrue(balancer.enqueue(WAREHOUSE, STATION, "c-new"));

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


[REDACTED]

StrReplace

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

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

        Assert.assertEquals(3L, balancer.queueSize(WAREHOUSE, STATION));
        List<String> chains = new ArrayList<>(balancer.listQueuedChains(WAREHOUSE, STATION));
        Collections.sort(chains);
        Assert.assertEquals(Arrays.asList("chain-1", "chain-2", "chain-3"), chains);

        Assert.assertEquals(3L, balancer.queueSize(WAREHOUSE, STATION));
        List<String> chains = new ArrayList<>(balancer.listQueuedChains(WAREHOUSE, STATION));
        Collections.sort(chains);
        Assert.assertEquals(new ArrayList<>(chains),
                new ArrayList<String>() {{
                    add("chain-1");
                    add("chain-2");
                    add("chain-3");
                }});


[REDACTED]

Shell

sed -n '14p' /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot/baoshi-robot-test/pom.xml | od -c | head

Show exact bytes on line 14 of pom.xml


[REDACTED]

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

Run unit tests then integration tests with Redis


[REDACTED]

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-core install -DskipTests -q && jar tf baoshi-robot-core/target/baoshi-robot-core-*.jar | grep StationChainQueueBalancer | head

Verify core JAR contains StationChainQueueBalancer


[REDACTED]

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-test dependency:tree -Dverbose=false 2>/dev/null | head -40

Show baoshi-robot-test dependency tree


[REDACTED]

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

Run integration tests with full permissions for m2/reactor


精简启动：在测试应用中通过 `excludeName` 排除 Nacos、MQ 等自动配置（`robot-core` 经 `robot-common` 引入完整云依赖）。正在更新 `RobotIntegrationTestApplication` 与 `application-integration.yml`。

[REDACTED]

Write

package com.baoshi.robot.test.support;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 集成测试专用启动类：仅扫描 Redis 与 robot-core，并排除 Nacos / RocketMQ 等自动配置（避免依赖注册中心与 MQ）。
 * Redis 使用 {@code application-integration.yml} 指向真实实例（或通过环境变量覆盖）。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@SpringBootApplication(
        scanBasePackages = {"com.baoshi.redis", "com.baoshi.robot.core"},
        excludeName = {
                // 由 baoshi-robot-common 传递引入，集成测试无需连接
                "com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration",
                "com.alibaba.cloud.nacos.NacosConfigAutoConfiguration",
                "com.baoshi.mq.spring.boot.autoconfigure.MqAutoConfigure",
                "org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration"
        })
public class RobotIntegrationTestApplication {

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

# 集成测试：需可达 Redis；不连 Nacos / RocketMQ
spring:
  main:
    web-application-type: none
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
  cloud:
    nacos:
      config:
        enabled: false
      discovery:
        enabled: false

logging:
  level:
    root: WARN


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-test -am verify -Pintegration -q 2>&1 | tail -50

Re-run integration tests after excluding auto-configs


正在将测试配置合并为 `src/test/resources/application.yml`，确保 `spring.redis.*` 在创建 `RedisConfig` 前即可解析。

[REDACTED]

Write

# baoshi-robot-test 默认配置：真实 Redis（可用环境变量覆盖）；不启 Nacos
spring:
  main:
    web-application-type: none
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
  cloud:
    nacos:
      config:
        enabled: false
      discovery:
        enabled: false

logging:
  level:
    root: WARN

StrReplace

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = RobotIntegrationTestApplication.class)
public class StationChainQueueBalancerIT {

@SpringBootTest(classes = RobotIntegrationTestApplication.class)
@ActiveProfiles("integration")
public class StationChainQueueBalancerIT {


[REDACTED]

@SpringBootTest(classes = RobotIntegrationTestApplication.class)
@ActiveProfiles("zzw")
public class StationChainQueueBalancerIT {

@SpringBootTest(classes = RobotIntegrationTestApplication.class)
public class StationChainQueueBalancerIT {


[REDACTED]

Shell

cd /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot && mvn -pl baoshi-robot-test -am verify -Pintegration -q 2>&1 | tail -25

Run integration tests after fixing application.yml


[REDACTED]

StrReplace

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <!-- *IT.java 仅由 profile integration 的 failsafe 执行 -->
                    <excludes>
                        <exclude>**/*IT.java</exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <!-- 启用方式：mvn -pl baoshi-robot-test verify -Pintegration（需本机 Redis 可连） -->

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <!-- 集成测试使用 *IT.java，由 failsafe + profile integration 执行 -->
                    <excludes>
                        <exclude>**/*IT.java</exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>


【执行方案】

已新增独立模块 **`baoshi-robot-test`**，用**真实 Redis**（Lettuce + Redisson + Lua）做集成测试，**不对 `StationChainQueueBalancer` 使用 Mock**。

### 1. 父工程 `baoshi-robot/pom.xml`
- 在 `<modules>` 中加入 **`baoshi-robot-test`**。

### 2. 模块 `baoshi-robot-test/pom.xml`
- 依赖 **`baoshi-robot-core`**、`**spring-boot-starter-test**`（test）。
- **Surefire**：排除 `**/*IT.java`，避免默认 `mvn test` 跑集成测试。
- **`integration` profile**：绑定 **Failsafe**，只在该 profile 下跑 `**/*IT.java`。

### 3. 精简启动类 `RobotIntegrationTestApplication`
- 只扫描 **`com.baoshi.redis`、`com.baoshi.robot.core`**。
- **`excludeName`**：关闭 Nacos 发现/配置、本项目 **MqAutoConfigure**、**RocketMQAutoConfiguration**，避免必须连注册中心和 MQ。
- **`web-application-type=none`**：不启 Web 容器。

### 4. 测试配置 `src/test/resources/application.yml`
- **`spring.redis.*`**：默认 `127.0.0.1:6379`，可用 **`REDIS_HOST` / `REDIS_PORT` / `REDIS_PASSWORD`** 指向你的测试 Redis。
- **`spring.cloud.nacos.*.enabled=false`** 双保险。

### 5. 示例集成测试 `StationChainQueueBalancerIT`
- 使用独立仓库/站点键，**`@After` 删除**对应 Redis key，减少对环境影响。

### 如何执行

```bash
cd baoshi-robot
mvn -pl baoshi-robot-test -am verify -Pintegration
```

**需本机或环境变量能连上 Redis**；未启动 Redis 时会出现 `Connection refused`（本次在沙箱中已验证上下文能拉起，仅因无 Redis 进程而失败）。

### 说明
- 与 **`baoshi-robot-application`** 全量启动相比，本模块**刻意**不拉 Nacos/MQ，只满足「真实 Redis + 真实 `StationChainQueueBalancer`」的集成验证；若你后续要在「与线上一致的完整 Spring 环境」里测，可再在该模块增加基于 **`RobotApplication`** + 指定 profile 的用例。
- 默认 **`mvn install`** 不启用 `integration` profile 时，**不会执行** Failsafe 集成测试，避免 CI 无 Redis 时整体失败。

[REDACTED]


<user_query>
参考robot-application,配置是通过nacos读取的
</user_query>


将 `baoshi-robot-test` 的配置改为与 `robot-application` 一致，通过 Nacos 拉取配置。

[REDACTED]


[REDACTED]


正在把 `baoshi-robot-test` 改为与 `robot-application` 相同的 Nacos 引导方式，并移除会禁用 Nacos 的本地覆盖与排除项。

[REDACTED]

Write

# 与 baoshi-robot-application 一致：从 Nacos 拉取 system.yml、baoshi-robot.yml（namespace=baoshi-${spring.profiles.active}）
spring:
  application:
    name: baoshi-robot
  cloud:
    inetutils:
      ignored-interfaces:
        - VMware.*
      use-only-site-local-interfaces: true
    nacos:
      discovery:
        server-addr: nacos-server-addr:8848
        namespace: baoshi-${spring.profiles.active}
      config:
        server-addr: nacos-server-addr:8848
        namespace: baoshi-${spring.profiles.active}
        extension-configs:
          - data-id: system.yml
            refresh: true
          - data-id: ${spring.application.name}.yml
            refresh: true

log:
  root:
    path: ./log

# 集成测试仅补充与 application 不同的项；Redis 等由 Nacos 下发的配置提供
spring:
  main:
    web-application-type: none
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:zzw}

logging:
  level:
    root: WARN

package com.baoshi.robot.test.support;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 集成测试启动类：扫描范围与线上机器人服务中的 Redis + core 一致；
 * 配置与 {@link com.baoshi.robot.app.RobotApplication} 相同，经 {@code bootstrap.yml} 从 Nacos 拉取。
 *
 * @author zouzhiwen
 * @date 2026/05/07
 */
@SpringBootApplication(scanBasePackages = {"com.baoshi.redis", "com.baoshi.robot.core"})
public class RobotIntegrationTestApplication {

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

StrReplace

 * 执行：{@code mvn -pl baoshi-robot-test verify -Pintegration}<br>
 * 依赖本机或环境变量指向的 Redis：{@code REDIS_HOST}、{@code REDIS_PORT}、{@code REDIS_PASSWORD}

 * 执行：{@code mvn -pl baoshi-robot-test verify -Pintegration}<br>
 * 配置与 {@code baoshi-robot-application} 一致：{@code bootstrap.yml} 从 Nacos 读取 system.yml / baoshi-robot.yml；
 * 需可达 Nacos（默认 {@code nacos-server-addr:8848}）及配置中的 Redis；可用 {@code SPRING_PROFILES_ACTIVE} 切换命名空间。


[REDACTED]

Shell

sed -n '14p' /Users/kazusa/IdeaProjects/baoshi/wms/baoshi-robot/baoshi-robot-test/pom.xml | od -c

Verify pom.xml name tag bytes


[REDACTED]

StrReplace

<description>机器人服务集成测试（Nacos 拉取配置 + 真实 Redis，不使用 Mock）</description>

<description>机器人服务集成测试（连接真实 Redis 等环境，不使用 Mock）</description>

<!-- mvn -pl baoshi-robot-test verify -Pintegration（需 Nacos + Redis 与 application 一致可达） -->

<!-- 启用方式：mvn -pl baoshi-robot-test verify -Pintegration（需本机 Redis 可连） -->


将 `spring.profiles.active` 移入 `bootstrap.yml`，确保 Nacos 的 `namespace: baoshi-${spring.profiles.active}` 在拉取配置前已解析。

[REDACTED]

# 与 baoshi-robot-application 一致：从 Nacos 拉取 system.yml、baoshi-robot.yml（namespace=baoshi-${spring.profiles.active}）
spring:
  application:
    name: baoshi-robot
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:zzw}
  cloud:

# 与 baoshi-robot-application 一致：从 Nacos 拉取 system.yml、baoshi-robot.yml（namespace=baoshi-${spring.profiles.active}）
spring:
  application:
    name: baoshi-robot
  cloud:

# 集成测试仅补充与 application 不同的项；业务配置由 Nacos（bootstrap）下发
spring:
  main:
    web-application-type: none

logging:

# 集成测试仅补充与 application 不同的项；Redis 等由 Nacos 下发的配置提供
spring:
  main:
    web-application-type: none
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:zzw}

logging:


【执行方案】已按 **`baoshi-robot-application`** 的方式，让 **`baoshi-robot-test`** 通过 **Nacos** 读配置。

### 变更摘要

1. **`src/test/resources/bootstrap.yml`**（与 `robot-application` 的 `bootstrap.yml` 对齐）
   - `spring.application.name: baoshi-robot`
   - Nacos **discovery/config**：`server-addr`、`namespace: baoshi-${spring.profiles.active}`
   - **extension-configs**：`system.yml`、`baoshi-robot.yml`
   - **`spring.profiles.active: ${SPRING_PROFILES_ACTIVE:zzw}`** 写在 bootstrap 里，保证拉 Nacos 前就有 profile，命名空间为 `baoshi-zzw`（与现有 `ElevatorLoadBalancerTest` 习惯一致）

2. **`src/test/resources/application.yml`**
   - 去掉本地 `spring.redis` 和关闭 Nacos 的配置
   - 只保留 **`spring.main.web-application-type: none`**（集成测试不启 Web 端口）和日志级别

3. **`RobotIntegrationTestApplication`**
   - 去掉对 Nacos / MQ / RocketMQ 自动配置的 **`excludeName`**，避免挡掉与线上一致的配置加载

4. **`StationChainQueueBalancerIT` JavaDoc**
   - 说明依赖 **Nacos + Redis**（与 application 相同），可用 **`SPRING_PROFILES_ACTIVE`** 切换环境

5. **`baoshi-robot-test/pom.xml`**
   - 更新模块说明中的集成测试前置条件描述

### 本地/流水线使用方式

- 与跑 **`RobotApplication`** 一样：解析 **`nacos-server-addr`**（hosts 或 DNS）、能连 **`8848`**，且对应 namespace 里已有 **`system.yml` / `baoshi-robot.yml`**（含 `spring.redis` 等）。
- 切换环境：`SPRING_PROFILES_ACTIVE=xxx`（对应 Nacos namespace **`baoshi-xxx`**）。
- 执行：`mvn -pl baoshi-robot-test verify -Pintegration`

若你希望 **bootstrap 里的 Nacos 地址也支持环境变量覆盖**（例如 `NACOS_ADDR`），可以再说一下命名规范，我可以帮你改成与运维脚本一致的形式。

[REDACTED]