Files
aiot-platform-cloud/openspec/changes/add-video-module-wvp-migration/design.md
2026-04-05 18:18:53 +08:00

396 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design: WVP-Platform 迁移技术设计
**Change ID**: `add-video-module-wvp-migration`
---
## 1. 目录结构设计
```
viewsh-module-video/
├── viewsh-module-video-api/ # 契约层
│ └── com.viewsh.module.video/
│ ├── api/ # Feign RPC 接口(供其他模块调用)
│ └── enums/ # ApiConstants, ErrorCodeConstants
└── viewsh-module-video-server/ # 业务层
└── com.viewsh.module.video/
├── VideoServerApplication.java # 启动类
├── gb28181/ # ← WVP gb28181/ 原样保留内部结构
│ ├── SipLayer.java # SIP 协议栈初始化
│ ├── auth/ # Digest 认证
│ ├── bean/ # → 改名为 DO加注解
│ ├── controller/admin/ # → 移到 admin 包下
│ ├── dao/ # → Mapper 改继承 BaseMapperX
│ ├── event/ # 事件驱动
│ ├── service/ # 核心服务
│ ├── session/ # SIP 会话管理
│ ├── task/ # 设备心跳、订阅任务
│ ├── transmit/ # SIP 信令处理(保持不动)
│ └── utils/ # GB28181 工具类
├── media/ # ← WVP media/ 原样保留
│ ├── bean/ # MediaServer, StreamInfo
│ ├── event/ # 媒体事件
│ ├── service/ # IMediaServerService
│ ├── zlm/ # ZLMediaKit 集成
│ │ └── ZLMHttpHookListener.java # Hook @RestController
│ └── abl/ # ABL 适配器
├── aiot/ # ← WVP aiot/ AI 模块
│ ├── bean/
│ ├── controller/admin/
│ ├── dao/
│ └── service/
├── storager/ # ← Redis 缓存存储RedisCatchStorageImpl
│ ├── dao/ # MapperMediaServer/RecordPlan/CloudRecord 等)
│ └── dto/ # DTO
├── streamProxy/ # ← 拉流代理
├── streamPush/ # ← 推流管理
├── vmanager/ # ← 管理功能
│ ├── cloudRecord/
│ ├── recordPlan/
│ ├── rtp/
│ └── server/
├── service/ # ← 通用服务
├── common/ # ← 公共模块
├── utils/ # ← 工具类
└── framework/ # 框架配置层
├── sip/
│ └── SipConfig.java
├── media/
│ └── MediaConfig.java
├── redis/
│ ├── RedisTemplateConfig.java
│ ├── RedisMsgListenConfig.java
│ └── RedisRpcConfig.java
├── websocket/
│ └── WebSocketConfig.java
├── security/config/
│ └── SecurityConfiguration.java
└── config/
├── UserSetting.java
├── ThreadPoolTaskConfig.java
├── ScheduleConfig.java
└── CivilCodeFileConf.java
```
## 2. 包名映射表
```
com.genersoft.iot.vmp.gb28181.* → com.viewsh.module.video.gb28181.*
com.genersoft.iot.vmp.media.* → com.viewsh.module.video.media.*
com.genersoft.iot.vmp.aiot.* → com.viewsh.module.video.aiot.*
com.genersoft.iot.vmp.streamProxy.* → com.viewsh.module.video.streamProxy.*
com.genersoft.iot.vmp.streamPush.* → com.viewsh.module.video.streamPush.*
com.genersoft.iot.vmp.vmanager.* → com.viewsh.module.video.vmanager.*
com.genersoft.iot.vmp.service.* → com.viewsh.module.video.service.*
com.genersoft.iot.vmp.common.* → com.viewsh.module.video.common.*
com.genersoft.iot.vmp.utils.* → com.viewsh.module.video.utils.*
com.genersoft.iot.vmp.conf.SipConfig → com.viewsh.module.video.framework.sip.SipConfig
com.genersoft.iot.vmp.conf.MediaConfig → com.viewsh.module.video.framework.media.MediaConfig
com.genersoft.iot.vmp.conf.UserSetting → com.viewsh.module.video.framework.config.UserSetting
com.genersoft.iot.vmp.conf.redis.* → com.viewsh.module.video.framework.redis.*
com.genersoft.iot.vmp.conf.websocket.* → com.viewsh.module.video.framework.websocket.*
com.genersoft.iot.vmp.conf.ThreadPool* → com.viewsh.module.video.framework.config.*
com.genersoft.iot.vmp.conf.Schedule* → com.viewsh.module.video.framework.config.*
com.genersoft.iot.vmp.storager.* → com.viewsh.module.video.storager.*
```
## 3. DO 改造设计
### 3.1 DO 基类选择
直接使用框架标准基类,统一逻辑删除,不创建自定义 VideoBaseDO。
| 基类 | 适用表 | 提供字段 |
|------|--------|---------|
| **TenantBaseDO** | wvp_device, wvp_device_channel, wvp_device_alarm, wvp_device_mobile_position, wvp_platform, wvp_platform_channel, wvp_platform_group, wvp_platform_region, wvp_common_group, wvp_common_region, wvp_stream_proxy, wvp_stream_push, wvp_cloud_record, wvp_record_plan, wvp_record_plan_item, wvp_ai_* | tenantId, createTime, updateTime, creator, updater, deleted |
| **BaseDO** | wvp_media_server | createTime, updateTime, creator, updater, deleted无 tenantId |
### 3.1.1 硬删除改逻辑删除
WVP 原有 39 处 `DELETE FROM` 硬删除,统一改为 MyBatis Plus 逻辑删除:
```java
// WVP 原始(硬删除)
@Delete("DELETE FROM wvp_device WHERE device_id = #{deviceId}")
int delete(@Param("deviceId") String deviceId);
// 改造后(逻辑删除 — 自动改写为 UPDATE SET deleted=1
// 直接用 BaseMapperX 内置方法
mapper.delete(new LambdaQueryWrapperX<DeviceDO>()
.eq(DeviceDO::getDeviceId, deviceId));
```
### 3.1.2 唯一索引调整
逻辑删除后,被"删除"的记录仍然存在。如果表有唯一索引,再次插入相同数据会冲突。需要将 `deleted` 加入联合唯一索引:
```sql
-- 改造前
UNIQUE INDEX uk_device_id (device_id)
-- 改造后
UNIQUE INDEX uk_device_id (device_id, deleted)
```
需要调整唯一索引的核心表:
- `wvp_device``device_id`
- `wvp_device_channel``device_db_id + channel_id`
- `wvp_platform``server_gb_id`
- `wvp_media_server``id`(业务主键)
- `wvp_stream_proxy``app + stream`
- `wvp_stream_push``app + stream`
- `wvp_common_group``device_id`
- `wvp_common_region``device_id`
### 3.2 改造模式
```java
// WVP 原始
@Data
@Schema(description = "国标设备")
public class Device {
private int id;
private String deviceId;
private String name;
private String createTime; // String 类型
private String updateTime; // String 类型
...
}
// 改造后(业务表 — 多租户 + 逻辑删除)
@Data
@TableName("wvp_device")
@EqualsAndHashCode(callSuper = true)
public class DeviceDO extends TenantBaseDO {
// TenantBaseDO 提供: tenantId, createTime, updateTime, creator, updater, deleted
@TableId(type = IdType.AUTO)
private Integer id; // 保持 Integer不改 Long降低改造风险
private String deviceId;
private String name;
// 删除原有的 createTime/updateTimeTenantBaseDO 提供)
...
}
// 改造后(共享表 — 无租户,有逻辑删除)
@Data
@TableName("wvp_media_server")
@EqualsAndHashCode(callSuper = true)
public class MediaServerDO extends BaseDO {
// BaseDO 提供: createTime, updateTime, creator, updater, deleted
// 无 tenantId
@TableId(type = IdType.AUTO)
private Integer id;
...
}
```
### 3.3 特殊处理
- **DeviceChannel**86 字段):字段多但改造模式一致,批量处理
- **关联查询结果 DTO**:不继承 VideoBaseDO保持为普通 POJO因为是 JOIN 查询结果,不对应单表)
- **WVP 的 `int id`**:保持 `Integer id`,不改 Long。数据库也保持 `INT`,等稳定后再逐步升级
## 4. Mapper 改造设计
### 4.1 简单 CRUD → BaseMapperX 内置方法
```java
// WVP 原始
@Select("SELECT * FROM wvp_media_server WHERE id = #{id}")
MediaServer queryOne(String id);
@Insert("INSERT INTO wvp_media_server (...) VALUES (...)")
int add(MediaServer mediaServer);
// 改造后
@Mapper
public interface MediaServerMapper extends BaseMapperX<MediaServerDO> {
// 简单查询直接用继承方法
// selectById(), insert(), updateById() 等
default MediaServerDO selectByServerId(String id) {
return selectOne(MediaServerDO::getId, id);
}
}
```
### 4.2 复杂查询 → 保留 @Select 注解
```java
// 多表 JOIN、动态 SQL 保留注解方式
@Mapper
public interface DeviceChannelMapper extends BaseMapperX<DeviceChannelDO> {
// 复杂 JOIN 保留 @Select
@Select("<script>" +
"SELECT dc.*, d.device_id as parent_device_id " +
"FROM wvp_device_channel dc " +
"LEFT JOIN wvp_device d ON dc.device_db_id = d.id " +
"<where>" +
"<if test='query != null'> AND dc.name LIKE CONCAT('%',#{query},'%')</if>" +
"</where>" +
"</script>")
List<DeviceChannelDO> selectWithDevice(@Param("query") String query);
}
```
### 4.3 分页改造
```java
// WVP 原始PageHelper
PageHelper.startPage(page, count);
List<Device> devices = deviceMapper.getDevices(query);
return new PageInfo<>(devices);
// 改造后MyBatis Plus
Page<DeviceDO> page = new Page<>(pageNo, pageSize);
IPage<DeviceDO> result = deviceMapper.selectPage(page,
new LambdaQueryWrapperX<DeviceDO>()
.likeIfPresent(DeviceDO::getName, query));
return new PageResult<>(result.getRecords(), result.getTotal());
```
## 5. 认证替换设计
### 5.1 SecurityConfiguration 更新
```java
@Configuration("videoSecurityConfiguration")
public class SecurityConfiguration {
@Bean("videoAuthorizeRequestsCustomizer")
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(...registry) {
// 原有放行...
// WVP Hook 回调ZLM → WVP无用户认证
registry.requestMatchers("/index/hook/**").permitAll();
// 设备快照公开访问
registry.requestMatchers("/video/device/query/snap/**").permitAll();
// SSE 推送
registry.requestMatchers("/video/sse/**").permitAll();
}
};
}
}
```
### 5.2 用户获取替换
```java
// WVP: SecurityUtils.getUserInfo() 返回 LoginUser
// ↓ 替换为
// viewsh: SecurityFrameworkUtils.getLoginUserId() 返回 Long
```
## 6. 数据库设计
### 6.1 独立数据库 `aiot-video`
```yaml
# application-local.yaml
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://${MYSQL_HOST}:3306/aiot-video?...
```
### 6.2 表结构改造规则
每张 WVP 表需要:
1. 主键 `id INT AUTO_INCREMENT` 保持不变(不改 BIGINT降低风险
2. 业务表新增 `tenant_id BIGINT NOT NULL DEFAULT 0`(多租户隔离,共享表除外)
3. 新增 `creator VARCHAR(64) DEFAULT ''`
4. 新增 `updater VARCHAR(64) DEFAULT ''`
5. 新增 `deleted BIT(1) NOT NULL DEFAULT 0`(逻辑删除)
6. `create_time` / `update_time` 保留(类型改为 `DATETIME DEFAULT CURRENT_TIMESTAMP`
7. 含唯一索引的表,将 `deleted` 加入联合唯一索引(避免逻辑删除后重复插入冲突)
共享表wvp_media_server不加 `tenant_id`,需在 application.yaml 中配置:
```yaml
viewsh:
tenant:
ignore-tables:
- wvp_media_server
```
## 7. RedisTemplate 隔离设计
### 7.1 问题
viewsh 框架的 RedisTemplate 使用 Jackson 序列化(`RedisSerializer.json()`WVP 的 RedisTemplate 使用 FastJSON2 序列化(`GenericFastJsonRedisSerializer`)。两者 Bean 名冲突会导致其他模块 Redis 崩溃。
### 7.2 方案
```java
// WVP 原始 RedisTemplateConfig
@Bean
public RedisTemplate<Object, Object> redisTemplate(...) { ... }
// 改造后:独立 Bean 名称
@Configuration
public class VideoRedisTemplateConfig {
@Bean("videoRedisTemplate")
public RedisTemplate<Object, Object> videoRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// FastJSON2 序列化WVP 内部使用)
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(serializer);
template.setHashKeySerializer(RedisSerializer.string());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
```
### 7.3 影响范围
WVP 内部所有注入 RedisTemplate 的地方(约 30+ 处)需要改为:
```java
// 改造前
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
// 改造后
@Autowired
@Qualifier("videoRedisTemplate")
private RedisTemplate<Object, Object> redisTemplate;
```
主要涉及:`RedisRpcConfig``RedisCatchStorageImpl``RedisMsgListenConfig`、各 Listener 等。
## 8. Java 版本兼容性
WVP pom.xml 编译目标是 JDK 21但**经扫描确认 WVP 未使用 Java 21 专有语法**。仅用了 `String.isBlank()`Java 11 特性JDK 17 完全兼容。
处理方式:不需要语法降级,只需确保 video-server 的 pom.xml 不设置 `<source>21</source>`(继承根 pom 的 JDK 17 配置即可)。
## 9. 网关路由补充
Hook 回调需要额外路由ZLM 直接调用 video-server不经过网关认证
```yaml
# viewsh-gateway application.yaml
- id: video-hook
uri: grayLb://video-server
predicates:
- Path=/index/hook/**
```