396 lines
15 KiB
Markdown
396 lines
15 KiB
Markdown
# 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/ # Mapper(MediaServer/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/updateTime(TenantBaseDO 提供)
|
||
...
|
||
}
|
||
|
||
// 改造后(共享表 — 无租户,有逻辑删除)
|
||
@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/**
|
||
```
|