fix: oauth2文档
This commit is contained in:
179
OAuth2-Token接口使用文档.md
Normal file
179
OAuth2-Token接口使用文档.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# OAuth2 Token 接口使用文档
|
||||||
|
|
||||||
|
## 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
POST http://{域名}/system/oauth2/token
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通用规则
|
||||||
|
|
||||||
|
- 所有请求**必须**携带 HTTP Basic Auth 请求头:`Authorization: Basic base64(client_id:secret)`
|
||||||
|
- 响应格式:`{"code":0,"msg":"...","data":{...}}`
|
||||||
|
- `code=0` 表示成功
|
||||||
|
- 默认客户端:`client_id=default`、`secret=admin123`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的授权模式
|
||||||
|
|
||||||
|
### 1. 客户端模式 `client_credentials`(外部系统调用推荐)
|
||||||
|
|
||||||
|
**适用场景**:机器对机器调用,无需用户登录。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:48080/system/oauth2/token" \
|
||||||
|
-u "default:admin123" \
|
||||||
|
-d "grant_type=client_credentials"
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `grant_type` | 是 | 固定值 `client_credentials` |
|
||||||
|
| `scope` | 否 | 授权范围,多个用空格分隔,如 `read write` |
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"access_token": "a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
|
"refresh_token": "r1r2r3r4-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
|
"expires_in": 7200,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"scope": "read write",
|
||||||
|
"user_id": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 密码模式 `password`
|
||||||
|
|
||||||
|
**适用场景**:用户直接用账号密码换取 token(如移动端 App 登录)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:48080/system/oauth2/token" \
|
||||||
|
-u "default:admin123" \
|
||||||
|
-d "grant_type=password" \
|
||||||
|
-d "username=admin" \
|
||||||
|
-d "password=admin123"
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `grant_type` | 是 | 固定值 `password` |
|
||||||
|
| `username` | 是 | 用户账号 |
|
||||||
|
| `password` | 是 | 用户密码 |
|
||||||
|
| `scope` | 否 | 授权范围,多个用空格分隔 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 授权码模式 `authorization_code`
|
||||||
|
|
||||||
|
**适用场景**:第三方应用需要用户授权后才能访问用户数据(如 SSO 单点登录)。
|
||||||
|
|
||||||
|
分两步走:
|
||||||
|
|
||||||
|
**第一步**:用户浏览器访问授权页,拿到 `code`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
浏览器访问:
|
||||||
|
http://localhost:48080/system/oauth2/authorize?clientId=default
|
||||||
|
```
|
||||||
|
|
||||||
|
用户确认授权后,回调地址会带上 `code` 参数。
|
||||||
|
|
||||||
|
**第二步**:用 `code` 换 token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:48080/system/oauth2/token" \
|
||||||
|
-u "default:admin123" \
|
||||||
|
-d "grant_type=authorization_code" \
|
||||||
|
-d "code=xxxx" \
|
||||||
|
-d "redirect_uri=https://回调地址" \
|
||||||
|
-d "state=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `grant_type` | 是 | 固定值 `authorization_code` |
|
||||||
|
| `code` | 是 | 第一步获取的授权码 |
|
||||||
|
| `redirect_uri` | 是 | 必须与第一步的回调地址一致 |
|
||||||
|
| `state` | 否 | 透传的状态值,用于防 CSRF |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 刷新令牌 `refresh_token`
|
||||||
|
|
||||||
|
**适用场景**:token 快过期时,用 `refresh_token` 换新 token,无需重新登录。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:48080/system/oauth2/token" \
|
||||||
|
-u "default:admin123" \
|
||||||
|
-d "grant_type=refresh_token" \
|
||||||
|
-d "refresh_token=r1r2r3r4-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||||
|
```
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `grant_type` | 是 | 固定值 `refresh_token` |
|
||||||
|
| `refresh_token` | 是 | 之前获取的 refresh_token |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 其他接口
|
||||||
|
|
||||||
|
### 校验 Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:48080/system/oauth2/check-token" \
|
||||||
|
-u "default:admin123" \
|
||||||
|
-d "token=要校验的access_token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 撤销 Token(登出)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE "http://localhost:48080/system/oauth2/token?token=要撤销的access_token" \
|
||||||
|
-u "default:admin123"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四种模式对比
|
||||||
|
|
||||||
|
| 模式 | grant_type 值 | 是否需用户参与 | 适用场景 |
|
||||||
|
|------|-------------|-------------|---------|
|
||||||
|
| 客户端模式 | `client_credentials` | 否 | 外部系统调用、定时任务、机器间通信 |
|
||||||
|
| 密码模式 | `password` | 是(提供账号密码) | 移动端 App 登录、信任的客户端 |
|
||||||
|
| 授权码模式 | `authorization_code` | 是(浏览器确认授权) | 第三方应用 SSO、开放平台 |
|
||||||
|
| 刷新令牌 | `refresh_token` | 否 | Token 续期,配合以上任意模式使用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 外部系统完整调用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
① 你在后台创建 OAuth2 客户端,拿到 client_id + secret
|
||||||
|
│
|
||||||
|
② 外部系统用 client_credentials 模式换 token
|
||||||
|
curl -u "client_id:secret" -d "grant_type=client_credentials"
|
||||||
|
│
|
||||||
|
③ 拿到 access_token,之后每个请求带上
|
||||||
|
curl -H "Authorization: Bearer <access_token>" /admin-api/xxx
|
||||||
|
│
|
||||||
|
④ token 过期前,用 refresh_token 续期
|
||||||
|
curl -u "client_id:secret" -d "grant_type=refresh_token" -d "refresh_token=xxx"
|
||||||
|
```
|
||||||
@@ -60,3 +60,24 @@ tenant-id: {{adminTenantId}}
|
|||||||
POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
|
POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
|
||||||
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
|
||||||
tenant-id: {{adminTenantId}}
|
tenant-id: {{adminTenantId}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@host = http://localhost:48080
|
||||||
|
@clientId = huachuang-acs
|
||||||
|
@clientSecret = 1001open
|
||||||
|
|
||||||
|
### 1、客户端模式 client_credentials(你最开始curl对应的请求)
|
||||||
|
POST http://localhost:48080/system/oauth2/token
|
||||||
|
Authorization: Bearer 32a8c752af4947d392809f954e32fadf
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://localhost:48080/system/oauth2/token
|
||||||
|
Authorization: Basic default admin123
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
grant_type = client_credentials
|
||||||
|
|
||||||
|
###
|
||||||
@@ -32,6 +32,8 @@ public class SecurityConfiguration {
|
|||||||
.requestMatchers("/actuator/**").permitAll();
|
.requestMatchers("/actuator/**").permitAll();
|
||||||
// RPC 服务的安全配置
|
// RPC 服务的安全配置
|
||||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||||
|
// OAuth2 开放接口,无需登录即可访问
|
||||||
|
registry.requestMatchers("/admin-api/system/oauth2/**").permitAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
451
决策管理模块-数据库表字段分析.md
Normal file
451
决策管理模块-数据库表字段分析.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
# decision_manage(决策管理)模块 — 数据库表字段详解
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`decision_manage` 模块涉及两张核心数据库表:
|
||||||
|
|
||||||
|
- **`st_strategy_config`**(仓储策略配置表):定义每个具体策略的元信息和运行参数
|
||||||
|
- **`st_sect_strategy`**(库区策略关联表):定义每个库区绑定了哪些策略,形成策略链
|
||||||
|
|
||||||
|
两张表通过 `strategy_code`(策略编码)字段产生逻辑关联——`st_sect_strategy.strategy` 中存储的 JSON 数组元素值,对应 `st_strategy_config.strategy_code`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、`st_strategy_config`(仓储策略配置表)
|
||||||
|
|
||||||
|
**作用**:存储每一个可用策略的完整定义,包括策略的基本信息、分类、运行参数和启用状态。每个策略对应一个 Spring Bean(继承 `Decisioner` 的处理器),系统启动时通过 `strategy_code` 与 Bean 名称自动关联。
|
||||||
|
|
||||||
|
### 字段详解
|
||||||
|
|
||||||
|
#### 1. `id` — 策略标识(主键)
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
| 主键策略 | 无自增,手动赋值 |
|
||||||
|
| 生成方式 | `IdUtil.getStringId()`(雪花ID或UUID变体) |
|
||||||
|
|
||||||
|
**作用**:策略记录的唯一标识,由后端在新增时自动生成,前端不感知。`create` 方法中通过 `IdUtil.getStringId()` 赋值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. `strategy_code` — 策略编码
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR(64)` |
|
||||||
|
|
||||||
|
**作用**:策略的编码标识,是两张表之间的**核心关联字段**。每个策略编码与 Spring 容器中一个 `Decisioner` 子类的 `@Service("xxx")` Bean 名称严格一一对应。
|
||||||
|
|
||||||
|
**取值示例**:
|
||||||
|
| 策略编码 | 对应处理器 | 分类 |
|
||||||
|
|----------|-----------|------|
|
||||||
|
| `fifo` | FIFORuleHandler | base |
|
||||||
|
| `nearby` | NearbyRuleHandler | base |
|
||||||
|
| `weight` | WeightRuleHandler | base |
|
||||||
|
| `cluster` | ClusterRuleHandler | base |
|
||||||
|
| `limitStorage` | LimitStorageRuleHandler | base |
|
||||||
|
| `alleyAve` | AlleyAveRuleHandler | base |
|
||||||
|
| `depthPriority` | DepthPriorityHandler | diy |
|
||||||
|
| `fifo2` | FIFO2RuleHandler | diy |
|
||||||
|
| `inventory` | InventoryRuleHandler | diy |
|
||||||
|
| `passRCL` | PassRCLHandler | diy |
|
||||||
|
|
||||||
|
**注意**:当前 `create` 方法中硬编码为 `"000"`,这意味着新增策略时需要在数据库中手动修正 `strategy_code` 的值,或在界面上提供修改入口。这是一个应改进的点。
|
||||||
|
|
||||||
|
**关联关系**:被 `st_sect_strategy.strategy` JSON 数组引用,也被 `Decisioner.afterPropertiesSet()` 用于查询自身配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. `strategy_name` — 策略名称
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:策略的中文显示名称,用于后台管理界面展示,如"先进先出"、"就近放置"、"巷道均衡"等。
|
||||||
|
|
||||||
|
**约束**:`create` 方法中会校验名称唯一性——如果已存在相同名称的策略,抛出 `BadRequestException("已存在相同名称的策略【xxx】")`。
|
||||||
|
|
||||||
|
**使用场景**:在 `GET /api/strategy/decisionColumns` 接口中作为下拉选项的 `label` 字段返回给前端。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. `strategy_type` — 策略类型
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:标识策略适用于哪种仓储操作场景。
|
||||||
|
|
||||||
|
**取值约定**:
|
||||||
|
|
||||||
|
| 值 | 含义 | 典型策略 |
|
||||||
|
|----|------|----------|
|
||||||
|
| `1` | 入库策略 | limitStorage, nearby, alleyAve, weight, cluster, depthPriority, inventory |
|
||||||
|
| `2` | 出库策略 | fifo, fifo2 |
|
||||||
|
| `3` | 通用策略(出入库均可) | alleyAve(同时支持出入) |
|
||||||
|
|
||||||
|
**使用场景**:上层调度模块根据当前任务是入库还是出库,筛选对应类型的策略进行链式调用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. `class_type` — 类处理类型
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:标识策略实现类的归属分类,用于前端分组展示或权限控制。
|
||||||
|
|
||||||
|
**取值约定**:
|
||||||
|
|
||||||
|
| 值 | 含义 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| `base` | 基础策略 | 通用性强的标准策略,如 FIFO、就近、限位、均衡等 |
|
||||||
|
| `diy` | 自定义策略 | 为特定项目/现场定制的策略,如深位优先、双叉分配等 |
|
||||||
|
|
||||||
|
**代码中的实际使用**:当前在 Java 代码中未发现直接使用该字段做业务逻辑判断,主要用于界面上的分类筛选和展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. `param` — 策略参数
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR`(存储 JSON 字符串) |
|
||||||
|
|
||||||
|
**作用**:策略运行时可配置的参数,以 JSON 字符串形式存储。这是**策略配置最核心的字段**,直接影响策略的执行行为。
|
||||||
|
|
||||||
|
**各策略的 param 格式与含义**:
|
||||||
|
|
||||||
|
| 策略 | param 格式 | 含义 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| alleyAve (巷道均衡) | `["x","z","y"]` | 排序优先级:先按排(x),再按层(z),最后按列(y) |
|
||||||
|
| limitStorage (货位限位) | `["h","w","l","weight"]` | 需要匹配的维度:高度、宽度、深度、承重 |
|
||||||
|
| passRCL (排/列/层过滤) | `{"y":[1,2,3,104,103,102]}` | 排除列号为 1,2,3,102,103,104 的货位 |
|
||||||
|
| nearby (就近放置) | 当前未使用 param | 使用硬编码的 Top10,可扩展为从 param 读取候选数量 |
|
||||||
|
| weight (轻上重下) | 通过 handler 参数传递 | 泛型声明为 String,实际解析为 JSON |
|
||||||
|
|
||||||
|
**注意**:同一策略类可以在 `st_strategy_config` 中存在多条记录(不同 `strategy_code`),每条记录配置不同的 `param`,实现同一策略逻辑在不同场景下的差异化复用。例如不同库区可以有不同的排序维度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. `form_data` — 策略表单配置
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `JSONObject` |
|
||||||
|
| 数据库类型 | `VARCHAR`(通过 `FastjsonSortTypeHandler` 序列化) |
|
||||||
|
| 默认值 | `new JSONObject()`(空 JSON 对象) |
|
||||||
|
| TypeHandler | `FastjsonSortTypeHandler.class` |
|
||||||
|
|
||||||
|
**作用**:存储策略在前端表单中的渲染配置,例如表单字段定义、校验规则、下拉选项等。这是一个**纯前端辅助字段**,不参与后端决策逻辑。
|
||||||
|
|
||||||
|
**示例**:可能包含类似以下结构的数据(具体格式取决于前端表单设计器):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{"name": "x", "label": "排", "type": "checkbox"},
|
||||||
|
{"name": "y", "label": "列", "type": "checkbox"},
|
||||||
|
{"name": "z", "label": "层", "type": "checkbox"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**与 `param` 的区别**:
|
||||||
|
- `param`:策略的**运行参数**,后端 `handler()` 方法中解析并使用
|
||||||
|
- `form_data`:策略的**表单配置**,前端渲染配置界面时使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. `remark` — 描述/备注
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:策略的补充说明文本,用于记录策略的用途、注意事项、变更历史等。纯展示字段,不参与业务逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 9. `is_used` — 是否启用
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `Boolean` |
|
||||||
|
| 数据库类型 | `TINYINT` 或 `BIT` |
|
||||||
|
| 默认值 | `null`(未设置,建议默认为 `true` 或 `false`) |
|
||||||
|
|
||||||
|
**作用**:控制策略的启用/禁用状态。只有启用的策略才会在策略链中被调用。提供 `PUT /api/strategy/changeActive` 接口进行切换。
|
||||||
|
|
||||||
|
**切换逻辑**(`changeActive` 方法):每次调用将 `is_used` 值取反,实现一键启用/禁用切换。
|
||||||
|
|
||||||
|
**与 `ban` 的区别**:`is_used` 是实际在使用的启用状态控制字段;`ban` 字段在代码中未被使用,疑似冗余。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. `ban` — 是否禁用
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `Boolean` |
|
||||||
|
| 数据库类型 | `TINYINT` 或 `BIT` |
|
||||||
|
|
||||||
|
**作用**:从命名推断用于禁用标记,但在当前 Java 代码中**未找到任何读写该字段的逻辑**。可能是早期设计预留的字段,后来被 `is_used` 替代,或者是数据库中有但代码中未同步使用。
|
||||||
|
|
||||||
|
**建议**:确认是否在 SQL 脚本或前端有使用,如果确认废弃则应清理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 11. `update_name` — 修改人名称
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:记录最后一次修改该策略配置的操作人姓名。在 `update`、`deleteAll`、`changeActive` 方法中通过 `SecurityUtils.getCurrentNickName()` 自动赋值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 12. `update_time` — 修改时间
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:记录最后一次修改的时间戳。在 `update`、`deleteAll`、`changeActive` 方法中通过 `DateUtil.now()` 自动赋值。
|
||||||
|
|
||||||
|
**注意**:使用 `String` 而非 `LocalDateTime` 或 `Date`,实际存储格式取决于 `DateUtil.now()` 的返回值(通常是 `yyyy-MM-dd HH:mm:ss`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 13. `is_delete` — 是否删除(逻辑删除标记)
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `Boolean` |
|
||||||
|
| 数据库类型 | `TINYINT` 或 `BIT` |
|
||||||
|
| 查询默认值 | `Boolean.FALSE`(在 `StrategyQuery` 中设置) |
|
||||||
|
|
||||||
|
**作用**:逻辑删除标记。删除策略时不会物理删除记录,而是将该字段设为 `"1"`(注意代码中使用字符串 `"1"` 而非布尔值 `true`,存在类型不一致)。
|
||||||
|
|
||||||
|
**使用方式**:
|
||||||
|
- 查询时:Mapper XML 中硬编码过滤条件 `is_delete = "0"`,只查询未删除记录
|
||||||
|
- 删除时:`deleteAll` 方法通过 `UpdateWrapper` 设置 `is_delete = "1"`
|
||||||
|
|
||||||
|
**注意**:`StrategyQuery` 中虽然设置了 `is_delete = Boolean.FALSE` 作为默认参数,但 Mapper XML 中直接硬编码了 `is_delete = "0"`,Query 对象中的 `is_delete` 实际上没有在 SQL 中被引用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、`st_sect_strategy`(库区策略关联表)
|
||||||
|
|
||||||
|
**作用**:定义每个库区与策略链的绑定关系。一个库区可以绑定多个策略,这些策略按数组顺序组成策略链,入库/出库时依次执行。
|
||||||
|
|
||||||
|
### 字段详解
|
||||||
|
|
||||||
|
#### 1. `id` — 主键
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `Integer` |
|
||||||
|
| 数据库类型 | `INT` |
|
||||||
|
| 主键策略 | `IdType.AUTO`(数据库自增) |
|
||||||
|
|
||||||
|
**作用**:记录的唯一标识,由数据库自动生成。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. `sect_code` — 库区编码
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:标识该策略链配置属于哪个库区。库区是仓库物理空间的逻辑划分,每个库区有独立的编码(如 `A01`、`B02` 等)。
|
||||||
|
|
||||||
|
**使用场景**:
|
||||||
|
- 调度模块接到入库/出库任务时,根据目标库区编码查询该库区绑定的策略链
|
||||||
|
- `SectStrategyQuery` 支持按 `sect_code` 过滤分页查询
|
||||||
|
|
||||||
|
**关联**:库区编码对应基础数据模块中库区/仓位主数据,本表不存储库区本身的详细信息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. `strategy` — 策略编码列表(策略链)
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `List<String>` |
|
||||||
|
| 数据库类型 | `VARCHAR`(存储 JSON 数组字符串) |
|
||||||
|
| TypeHandler | `ListStrTypeHandler.class` |
|
||||||
|
|
||||||
|
**作用**:这是决策模块最核心的字段,定义了该库区的**策略链**——即入库/出库时依次执行的策略编码列表,**数组顺序即为执行顺序**。
|
||||||
|
|
||||||
|
**存储格式**(数据库中的实际内容):
|
||||||
|
```json
|
||||||
|
["limitStorage", "nearby", "alleyAve"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Java 中的表示**:
|
||||||
|
```java
|
||||||
|
List<String> strategy = ["limitStorage", "nearby", "alleyAve"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行流程**:当任务到达该库区时,调度模块会按数组顺序依次调用对应的策略处理器:
|
||||||
|
|
||||||
|
```
|
||||||
|
可用货位列表
|
||||||
|
→ limitStorage.handler() // 过滤尺寸/重量不匹配的货位
|
||||||
|
→ nearby.handler() // 按距离排序,取前N个
|
||||||
|
→ alleyAve.handler() // 巷道均衡分配
|
||||||
|
→ 最终货位列表
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeHandler 转换机制**:
|
||||||
|
|
||||||
|
- **写入时**(Java → DB):`ListStrTypeHandler.setNonNullParameter()` 调用 `JSON.toJSONString(parameter)`,将 `List<String>` 序列化为 JSON 数组字符串
|
||||||
|
- **读取时**(DB → Java):`ListTypeHandler.getNullableResult()` 调用 `JSONArray.parseArray(s, String.class)`,将 JSON 数组反序列化为 `List<String>`;若为 `NULL` 则返回空 `ArrayList`
|
||||||
|
|
||||||
|
**策略编码的取值**:列表中的每个元素必须是 `st_strategy_config.strategy_code` 的有效值,对应一个已注册的 Spring Bean。如果填写了不存在的策略编码,运行时才会报错。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. `strategy_type` — 策略类型
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:标识该条策略链配置适用于哪种操作场景。
|
||||||
|
|
||||||
|
**取值约定**:
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| `1` | 入库策略链 |
|
||||||
|
| `2` | 出库策略链 |
|
||||||
|
|
||||||
|
**使用场景**:一个库区可能同时需要入库和出库的策略链配置,通过该字段区分。例如:
|
||||||
|
|
||||||
|
```
|
||||||
|
库区 A01:
|
||||||
|
- strategy_type=1: ["limitStorage", "nearby", "alleyAve"] ← 入库用
|
||||||
|
- strategy_type=2: ["fifo"] ← 出库用
|
||||||
|
```
|
||||||
|
|
||||||
|
调度模块根据当前任务是入库还是出库,查询对应 `strategy_type` 的策略链。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. `description` — 描述
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:对该库区策略链配置的说明文本,用于后台管理界面的展示。纯文本备注,不参与业务逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. `update_time` — 更新时间
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:记录最后一次修改的时间。在 Controller 的 `create` 和 `update` 方法中通过 `DateUtil.now()` 手动赋值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 7. `update_name` — 更新人
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Java 类型 | `String` |
|
||||||
|
| 数据库类型 | `VARCHAR` |
|
||||||
|
|
||||||
|
**作用**:记录最后一次修改该库区策略配置的操作人。在 Controller 中通过 `SecurityUtils.getCurrentUsername()` 手动赋值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、两表关联关系
|
||||||
|
|
||||||
|
```
|
||||||
|
st_strategy_config (策略定义) st_sect_strategy (库区-策略绑定)
|
||||||
|
┌──────────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ id (PK) │ │ id (PK, AUTO) │
|
||||||
|
│ strategy_code ◄──────────┼──────────│ strategy (JSON数组) │
|
||||||
|
│ strategy_name │ 逻辑引用 │ sect_code │
|
||||||
|
│ strategy_type │ │ strategy_type │
|
||||||
|
│ class_type │ │ description │
|
||||||
|
│ param │ │ update_time │
|
||||||
|
│ form_data │ │ update_name │
|
||||||
|
│ remark │ └──────────────────────────┘
|
||||||
|
│ is_used │
|
||||||
|
│ ban │
|
||||||
|
│ update_name │
|
||||||
|
│ update_time │
|
||||||
|
│ is_delete │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**关联方式**:`st_sect_strategy.strategy` 中的 JSON 数组元素(如 `"fifo"`)逻辑上引用 `st_strategy_config.strategy_code`。
|
||||||
|
|
||||||
|
**当前限制**:由于使用 JSON 数组存储,数据库层面无法建立外键约束。如果策略编码被修改或删除,库区策略链中的引用不会自动更新或校验。
|
||||||
|
|
||||||
|
**查询链路**:
|
||||||
|
1. 调度模块根据任务中的库区编码 `sect_code` + 策略类型 `strategy_type` 查询 `st_sect_strategy`
|
||||||
|
2. 从 `strategy` 字段解析出策略编码列表
|
||||||
|
3. 按顺序从 Spring 容器中获取对应 Bean 名称的 `Decisioner` 实例
|
||||||
|
4. 每个 `Decisioner` 在初始化时已通过 `strategy_code` 从 `st_strategy_config` 加载了自身的配置(`param`、`form_data` 等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、字段补充说明
|
||||||
|
|
||||||
|
### 缺失的常见字段
|
||||||
|
|
||||||
|
两张表均**没有**明确声明以下常见审计字段(但代码中有隐式引用):
|
||||||
|
|
||||||
|
| 缺失字段 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `create_time` | 创建时间 — 实体中未声明,但 `StStrategyConfigServiceImpl.pageQuery` 中使用了 `ORDER BY create_time DESC`,说明数据库表中实际存在该字段,只是 Java 实体未映射 |
|
||||||
|
| `create_name` | 创建人 — 新增方法中未赋值,可能依赖数据库默认值 |
|
||||||
|
|
||||||
|
### 类型不一致问题
|
||||||
|
|
||||||
|
| 问题 | 位置 | 详情 |
|
||||||
|
|------|------|------|
|
||||||
|
| `is_delete` 赋值类型 | `StStrategyConfigServiceImpl.deleteAll()` | `Boolean` 字段被赋值为字符串 `"1"` |
|
||||||
|
| `update_time` 类型 | 两张表 | 使用 `String` 存储时间,而非 `LocalDateTime` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、设计改进建议(与字段相关)
|
||||||
|
|
||||||
|
1. **`strategy_code` 创建时硬编码 `"000"`**:`create` 方法未提供策略编码的输入入口,需要后续手动修改。建议在创建界面提供编码输入框,并增加编码唯一性校验。
|
||||||
|
|
||||||
|
2. **`ban` 字段废弃**:代码中未使用,确认后可清理。
|
||||||
|
|
||||||
|
3. **`is_delete` 赋值类型修复**:`deleteAll` 中应将 `"1"` 改为 `true`,保持与字段声明的 `Boolean` 类型一致。
|
||||||
|
|
||||||
|
4. **`update_time` 使用更合适的类型**:建议从 `String` 改为 `LocalDateTime`,利用 Java 8 时间 API 的类型安全优势。
|
||||||
|
|
||||||
|
5. **补充 `create_time` / `create_name` 映射**:数据库存在但 Java 实体未映射,导致新增记录时无法在实体层面追踪创建信息。
|
||||||
|
|
||||||
|
6. **`strategy` JSON 数组的引用完整性**:无法通过数据库约束保证 `st_sect_strategy.strategy` 中的策略编码在 `st_strategy_config` 中真实存在,建议在保存时增加后端校验逻辑。
|
||||||
393
决策管理模块-策略链执行器架构设计.md
Normal file
393
决策管理模块-策略链执行器架构设计.md
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
# 决策管理模块 — 策略链执行器架构设计
|
||||||
|
|
||||||
|
## 一、旧代码问题分析
|
||||||
|
|
||||||
|
### 1.1 当前 `outBoundSectDiv` / `inBoundSectDiv` 的代码
|
||||||
|
|
||||||
|
```java
|
||||||
|
// StructattrServiceImpl.java (旧项目)
|
||||||
|
public List<StrategyStructMaterialVO> outBoundSectDiv(StrategyStructParam param) {
|
||||||
|
StSectStrategy one = iStSectStrategyService.getOne(...);
|
||||||
|
List<String> strategy = one.getStrategy();
|
||||||
|
Map<String, Decisioner> decisionerMap = SpringContextHolder.getBeansOfType(Decisioner.class);
|
||||||
|
List list = null;
|
||||||
|
for (String decisionerType : strategy) {
|
||||||
|
Decisioner decisioner = decisionerMap.get(decisionerType);
|
||||||
|
list = decisioner.handler(list, param);
|
||||||
|
if (CollectionUtils.isEmpty(list)){
|
||||||
|
throw new BadRequestException("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 逐个问题分析
|
||||||
|
|
||||||
|
#### 问题一:`List list = null` 作为初始输入
|
||||||
|
|
||||||
|
`outBoundSectDiv` 将 `null` 作为初始列表传给第一个策略处理器。
|
||||||
|
|
||||||
|
以 `FIFORuleHandler` 为例,它**完全忽略输入参数 `list`**,在方法内部自行查询库存:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// FIFORuleHandler.handler() — 注意 list 参数从未被使用
|
||||||
|
public List<StrategyStructMaterialVO> handler(List<StrategyStructMaterialVO> list, StrategyStructParam param) {
|
||||||
|
// 自行查询库存,不依赖入参 list
|
||||||
|
List<StructattrVechielDto> vechielDtos = iStructattrService.collectVechicle(...);
|
||||||
|
// ... 处理并返回
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这导致了**两种不一致的初始数据加载模式**:
|
||||||
|
|
||||||
|
| 方法 | 初始数据来源 | 首个处理器行为 |
|
||||||
|
|------|------------|-------------|
|
||||||
|
| `inBoundSectDiv` | Service 层查询所有可用货位 → 传入链 | 处理器接收实际数据,进行筛选/排序 |
|
||||||
|
| `outBoundSectDiv` | 传 `null` | 首个处理器(FIFO)自行查询库存 |
|
||||||
|
|
||||||
|
**根因**:策略链没有统一的数据加载阶段,初始数据的获取逻辑散落在 Service 层和各个处理器之间。
|
||||||
|
|
||||||
|
#### 问题二:`SpringContextHolder.getBeansOfType(Decisioner.class)` 全量扫描
|
||||||
|
|
||||||
|
```java
|
||||||
|
Map<String, Decisioner> decisionerMap = SpringContextHolder.getBeansOfType(Decisioner.class);
|
||||||
|
```
|
||||||
|
|
||||||
|
**每次调用**都从 Spring 容器中扫描**所有** `Decisioner` 类型的 Bean(旧项目中有 10+ 个),然后从中取一个用。这相当于每次都把整个工具箱倒出来只为了拿一把螺丝刀。
|
||||||
|
|
||||||
|
**正确做法**:通过 Bean 名称精确查找。
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 应该这样做 — 按名称精确获取
|
||||||
|
DecisionManage<?, ?> decisioner = (DecisionManage<?, ?>) applicationContext.getBean("fifo");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 问题三:原生类型 `List` 丢失泛型信息
|
||||||
|
|
||||||
|
```java
|
||||||
|
List list = null; // 原生类型
|
||||||
|
list = decisioner.handler(list, param); // 隐式不安全转换
|
||||||
|
return list; // 返回值也是原生类型
|
||||||
|
```
|
||||||
|
|
||||||
|
`Decisioner<T, P>` 是泛型类,但使用时完全退化为原生类型。编译器无法检查类型安全,FIFO 返回 `List<StrategyStructMaterialVO>` 但赋值给 `List`,再返回为 `List<StrategyStructMaterialVO>` —— 全靠程序员记忆保证不出错。
|
||||||
|
|
||||||
|
#### 问题四:策略链编排逻辑重复
|
||||||
|
|
||||||
|
`inBoundSectDiv` 和 `outBoundSectDiv` 中策略链的加载、遍历、执行、空检查逻辑**完全重复**,只有初始数据加载方式不同:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 两段代码的重复部分
|
||||||
|
StSectStrategy one = iStSectStrategyService.getOne(...); // 查策略链配置
|
||||||
|
List<String> strategy = one.getStrategy(); // 解析策略列表
|
||||||
|
Map<String, Decisioner> decisionerMap = ...; // 获取所有Bean
|
||||||
|
for (String decisionerType : strategy) { // 遍历执行
|
||||||
|
list = decisioner.handler(list, param);
|
||||||
|
if (CollectionUtils.isEmpty(list)) { throw ...; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 问题五:空检查时机有逻辑漏洞
|
||||||
|
|
||||||
|
```java
|
||||||
|
list = decisioner.handler(list, param);
|
||||||
|
if (CollectionUtils.isEmpty(list)){
|
||||||
|
throw new BadRequestException("当前策略" + ... + "无可用货位,分配前数量:" + list.size());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
异常消息中的 `分配前数量` 实际是**分配后**的数量(handler 已经执行完了),且 "分配前" 语义模糊。另外,如果某个处理器确实应该返回空列表(如过滤后无符合条件的结果),这里直接抛异常阻止了整个链,没有给后续处理器降级处理的机会。
|
||||||
|
|
||||||
|
#### 问题六:无可观测性
|
||||||
|
|
||||||
|
- 无执行耗时记录(仅 `limitStorage` 和 `alleyAve` 内部有手动计时)
|
||||||
|
- 无输入/输出数量变化追踪
|
||||||
|
- 异常时只有简单的消息,无堆栈上下文
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、新架构设计
|
||||||
|
|
||||||
|
### 2.1 核心思想
|
||||||
|
|
||||||
|
引入 **`StrategyChainExecutor`**(策略链执行器)作为独立的编排层,将策略链的加载、查找、执行、监控统一封装,与业务 Service 解耦。
|
||||||
|
|
||||||
|
### 2.2 架构分层
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Controller 层 │
|
||||||
|
│ WarehouseStrategyController / 调度任务入口 │
|
||||||
|
└────────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────▼────────────────────────────────┐
|
||||||
|
│ Service 层 │
|
||||||
|
│ XxxServiceImpl │
|
||||||
|
│ 职责:加载初始候选数据(可用货位 / 库存记录) │
|
||||||
|
│ 调用 StrategyChainExecutor 执行策略链 │
|
||||||
|
└────────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────▼────────────────────────────────┐
|
||||||
|
│ StrategyChainExecutor(新增) │
|
||||||
|
│ 职责:① 查询库区策略链配置 │
|
||||||
|
│ ② 按 Bean 名称精确获取策略处理器 │
|
||||||
|
│ ③ 按序执行策略链 │
|
||||||
|
│ ④ 统一日志、耗时、异常处理 │
|
||||||
|
└────────────────────────┬────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────▼────────────────────────────────┐
|
||||||
|
│ DecisionManage<T, P>(策略处理器) │
|
||||||
|
│ 职责:单一维度的货位筛选/排序逻辑 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 类职责矩阵
|
||||||
|
|
||||||
|
| 类 | 职责 | 不该做的事 |
|
||||||
|
|----|------|-----------|
|
||||||
|
| **StrategyChainExecutor** | 编排策略链:查配置 → 找Bean → 依次执行 → 日志监控 | 不加载初始数据,不关心业务类型 |
|
||||||
|
| **DecisionManage<T,P>** | 实现单一策略逻辑(筛选/排序) | 不查策略链配置,不关心其他策略 |
|
||||||
|
| **XxxServiceImpl** | 加载初始候选数据,调用执行器 | 不直接操作策略Bean,不遍历策略链 |
|
||||||
|
| **WarehouseStrategyDO** | 库区→策略链的映射关系 | — |
|
||||||
|
| **WarehouseStrategyConfigDO** | 单个策略的参数配置 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、核心类设计
|
||||||
|
|
||||||
|
### 3.1 StrategyChainExecutor
|
||||||
|
|
||||||
|
```java
|
||||||
|
package cn.code.nl.module.wms.manage;
|
||||||
|
|
||||||
|
import cn.code.nl.framework.common.exception.ServiceException;
|
||||||
|
import cn.code.nl.module.wms.dal.dataobject.warehousestrategy.WarehouseStrategyDO;
|
||||||
|
import cn.code.nl.module.wms.dal.mysql.warehousestrategy.WarehouseStrategyMapper;
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 策略链执行器,负责按库区配置的策略链依次调用策略处理器
|
||||||
|
*
|
||||||
|
* @Author: liyongde
|
||||||
|
* @Date: 2026/7/18
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class StrategyChainExecutor {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WarehouseStrategyMapper warehouseStrategyMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行策略链
|
||||||
|
* <p>
|
||||||
|
* 根据库区编码和策略类型加载策略链配置,从 Spring 容器中按名称查找对应的策略处理器,
|
||||||
|
* 按序执行,每个处理器的输出作为下一个处理器的输入。
|
||||||
|
*
|
||||||
|
* @param sectionCode 库区编码
|
||||||
|
* @param strategyType 策略类型(1=入库,2=出库)
|
||||||
|
* @param candidates 初始候选列表(入库时为可用货位列表,出库时传空列表由首个策略自行查询)
|
||||||
|
* @param param 决策参数(物料信息、出入库类型等)
|
||||||
|
* @param <T> 货位/库存数据类型
|
||||||
|
* @return 策略链执行后的结果列表
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> List<T> execute(String sectionCode, String strategyType,
|
||||||
|
List<T> candidates, JSONObject param) {
|
||||||
|
// 1. 查询库区策略链配置
|
||||||
|
WarehouseStrategyDO strategy = warehouseStrategyMapper.selectOne(
|
||||||
|
WarehouseStrategyDO::getSectionCode, sectionCode,
|
||||||
|
WarehouseStrategyDO::getStrategyType, strategyType);
|
||||||
|
if (strategy == null) {
|
||||||
|
throw new ServiceException(500, "当前库区 " + sectionCode + " 未配置策略链");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析策略编码列表
|
||||||
|
List<String> strategyCodes = JSON.parseArray(strategy.getStrategy(), String.class);
|
||||||
|
if (CollUtil.isEmpty(strategyCodes)) {
|
||||||
|
throw new ServiceException(500, "当前库区 " + sectionCode + " 策略链为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 按序执行策略链
|
||||||
|
List<T> result = candidates;
|
||||||
|
for (String strategyCode : strategyCodes) {
|
||||||
|
DecisionManage<T, JSONObject> handler;
|
||||||
|
try {
|
||||||
|
handler = (DecisionManage<T, JSONObject>) applicationContext.getBean(strategyCode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("策略 [{}] 未找到对应的处理器 Bean", strategyCode);
|
||||||
|
throw new ServiceException(500, "策略 " + strategyCode + " 未注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
String strategyName = handler.strategyConfig.getStrategyName();
|
||||||
|
int inputSize = result != null ? result.size() : 0;
|
||||||
|
log.info("执行策略 [{}]:{},输入候选数量:{}", strategyCode, strategyName, inputSize);
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
result = handler.handler(result, param);
|
||||||
|
long cost = System.currentTimeMillis() - startTime;
|
||||||
|
|
||||||
|
int outputSize = result != null ? result.size() : 0;
|
||||||
|
log.info("策略 [{}] 执行完成,耗时:{}ms,输出数量:{}(减少:{})",
|
||||||
|
strategyCode, cost, outputSize, inputSize - outputSize);
|
||||||
|
|
||||||
|
if (CollUtil.isEmpty(result)) {
|
||||||
|
throw new ServiceException(500,
|
||||||
|
"策略 " + strategyName + " 执行后无可用货位");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 调用方的使用方式
|
||||||
|
|
||||||
|
入库和出库的调用代码变得简洁统一:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 入库分配
|
||||||
|
public List<Structattr> inBoundSectDiv(StrategyStructParam param) {
|
||||||
|
// 1. 加载初始候选数据(可用货位)
|
||||||
|
List<Structattr> candidates = structattrMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<Structattr>()
|
||||||
|
.eq(Structattr::getIsUsed, true)
|
||||||
|
.eq(Structattr::getLockType, "未锁定")
|
||||||
|
.eq(Structattr::getSectCode, param.getSectCode()));
|
||||||
|
if (CollUtil.isEmpty(candidates)) {
|
||||||
|
throw new ServiceException(500, "库区 " + param.getSectCode() + " 无可用货位");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 执行策略链
|
||||||
|
JSONObject params = (JSONObject) JSON.toJSON(param);
|
||||||
|
return strategyChainExecutor.execute(
|
||||||
|
param.getSectCode(), "1", candidates, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出库分配
|
||||||
|
public List<StrategyStructMaterialVO> outBoundSectDiv(StrategyStructParam param) {
|
||||||
|
// 出库的首个策略(FIFO)自行查询库存,传入空列表即可
|
||||||
|
JSONObject params = (JSONObject) JSON.toJSON(param);
|
||||||
|
return strategyChainExecutor.execute(
|
||||||
|
param.getSectCode(), "2", null, params);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、新旧代码对比
|
||||||
|
|
||||||
|
| 维度 | 旧代码 | 新架构 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| **编排职责** | 散落在 `StructattrServiceImpl` 中,与 CRUD 逻辑混在一起 | 独立的 `StrategyChainExecutor` 组件 |
|
||||||
|
| **Bean 查找** | `getBeansOfType()` 全量扫描 | `applicationContext.getBean(name)` 精确查找 |
|
||||||
|
| **类型安全** | 原生 `List`,无泛型约束 | `@SuppressWarnings("unchecked")` 显式标注,调用方强类型 |
|
||||||
|
| **日志** | 无统一日志,部分处理器内部有 | 每个策略执行前后记录:名称、耗时、输入/输出数量 |
|
||||||
|
| **空值处理** | 错误消息写"分配前"实际是"分配后" | 明确区分输入/输出数量 |
|
||||||
|
| **代码复用** | `inBoundSectDiv` / `outBoundSectDiv` 中策略遍历逻辑重复 | 统一由 `execute()` 处理 |
|
||||||
|
| **异常处理** | `BadRequestException`,无处理器查找失败的兜底 | `ServiceException`,区分"未配置策略链"和"处理器未注册" |
|
||||||
|
| **扩展性** | 新增分配类型需复制整段遍历代码 | 只需调用 `execute()` + 准备初始数据 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、数据流示意
|
||||||
|
|
||||||
|
### 入库流程
|
||||||
|
|
||||||
|
```
|
||||||
|
调用方:
|
||||||
|
① 查询库区所有可用货位(未锁定、未占用)
|
||||||
|
② 得到 List<Structattr>(如 200 个)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
StrategyChainExecutor.execute(sectionCode, "1", candidates, param):
|
||||||
|
③ 查 wms_warehouse_strategy → strategy = ["limitStorage","nearby","alleyAve"]
|
||||||
|
④ 依次执行:
|
||||||
|
ctx.getBean("limitStorage") → handler(200个, param) → 80个(过滤尺寸不匹配)
|
||||||
|
ctx.getBean("nearby") → handler(80个, param) → 10个(按距离取最近)
|
||||||
|
ctx.getBean("alleyAve") → handler(10个, param) → 10个(巷道均衡排序)
|
||||||
|
⑤ 返回最终 10 个货位
|
||||||
|
```
|
||||||
|
|
||||||
|
### 出库流程
|
||||||
|
|
||||||
|
```
|
||||||
|
调用方:
|
||||||
|
① 不预加载数据,传 null
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
StrategyChainExecutor.execute(sectionCode, "2", null, param):
|
||||||
|
③ 查 wms_warehouse_strategy → strategy = ["fifo"]
|
||||||
|
④ 依次执行:
|
||||||
|
ctx.getBean("fifo") → handler(null, param) → FIFO内部查库存 → 5个分配结果
|
||||||
|
⑤ 返回 5 个库存分配记录
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、扩展指南
|
||||||
|
|
||||||
|
### 新增一个策略
|
||||||
|
|
||||||
|
**Step 1**:创建策略处理器
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service("myStrategy") // Bean 名称 = 策略编码
|
||||||
|
@Slf4j
|
||||||
|
public class MyStrategyHandler extends DecisionManage<Structattr, JSONObject> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Structattr> handler(List<Structattr> list, JSONObject param) {
|
||||||
|
// 从配置读取参数
|
||||||
|
String configParam = this.strategyConfig.getParam();
|
||||||
|
// 实现筛选/排序逻辑
|
||||||
|
return filteredList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2**:在数据库注册策略配置
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO wms_warehouse_strategy_config
|
||||||
|
(strategy_code, strategy_name, strategy_type, class_type, param, is_used)
|
||||||
|
VALUES ('myStrategy', '我的策略', '1', 'diy', '{"threshold": 10}', 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3**:在库区策略链中加入新策略
|
||||||
|
|
||||||
|
将 `wms_warehouse_strategy.strategy` JSON 数组中追加 `"myStrategy"` 即可,无需修改任何 Java 代码。
|
||||||
|
|
||||||
|
### 新增一个分配场景
|
||||||
|
|
||||||
|
只需在 Service 层加载对应初始数据,调用 `strategyChainExecutor.execute()`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 例如:盘点分配
|
||||||
|
public List<Structattr> inventoryDiv(String sectionCode) {
|
||||||
|
List<Structattr> candidates = loadInventorySlots(sectionCode);
|
||||||
|
return strategyChainExecutor.execute(sectionCode, "3", candidates, new JSONObject());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、实施建议
|
||||||
|
|
||||||
|
1. **先实现 `StrategyChainExecutor`**,它是整个架构的枢纽,不依赖任何具体策略
|
||||||
|
2. **迁移现有策略处理器**:旧项目的 `FIFORuleHandler`、`NearbyRuleHandler` 等改为继承新的 `DecisionManage<T, P>`,`handler` 方法签名基本不变
|
||||||
|
3. **调整 Service 层调用**:将 `StructattrServiceImpl` 中的策略遍历代码替换为对 `StrategyChainExecutor.execute()` 的调用
|
||||||
|
4. **数据库映射**:旧表 `st_sect_strategy` → 新表 `wms_warehouse_strategy`,旧表 `st_strategy_config` → 新表 `wms_warehouse_strategy_config`,字段映射见之前的字段分析文档
|
||||||
Reference in New Issue
Block a user