第一阶段任务,相关数据库定义。
IT-0 verify / verify (push) Waiting to run

This commit is contained in:
zfjia committed 2026-11-10 14:52:54 +08:00
1 parent 329e27b515
commit 8ffeb59dd4
33 files changed
+3427 -111

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
name: IT-0 verify
on:
push:
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Compile and run unit tests
working-directory: dev/RuoYi-Vue-master
run: mvn -B -ntp test
- name: Reject plaintext credentials (IT-0 acceptance #2 / IR-05)
shell: bash
run: |
set -e
RES=dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources
if grep -RInE '(password|secret)[[:space:]]*:[[:space:]]*[^$<{[:space:]]' "$RES"; then
echo "::error::plaintext credential found in resources"
exit 1
fi
if grep -RInE 'APT_DB_PASSWORD[[:space:]]*=[[:space:]]*[^<$[:space:]]' . --exclude-dir=.git; then
echo "::error::a literal database password is committed"
exit 1
fi
if git ls-files --error-unmatch dev/.env >/dev/null 2>&1; then
echo "::error::dev/.env must not be tracked"
exit 1
fi
echo "credential scan passed"
+26
View File
@@ -0,0 +1,26 @@
# IT-0 / IR-05:凭据与本机环境一律不入库
.env
.env.local
.env.*.local
**/.env
**/application-local.yml
**/application-local.properties
**/application-secret.yml
dev/scripts/local-env.ps1
# 构建产物与依赖
target/
**/target/
node_modules/
**/node_modules/
dist/
*.class
*.log
*.jar
*.war
# IDE / 运行产物
.idea/
.vscode/
uploadPath/
.DS_Store
+22
View File
@@ -0,0 +1,22 @@
# 本地环境变量模板(可复制到 .env 后填写,.env 已被 .gitignore 排除)
# 说明:本文件只放占位符,禁止提交任何真实口令。
APT_DB_HOST=127.0.0.1
APT_DB_PORT=3306
APT_DB_NAME=apt_rev01
APT_DB_USER=iotlocks
APT_DB_PASSWORD=<从本机安全位置填入,勿提交>
APT_REDIS_HOST=127.0.0.1
APT_REDIS_PORT=6379
APT_REDIS_DB=1
APT_REDIS_PASSWORD=
APT_TOKEN_SECRET=<32 位以上随机串,可用 openssl rand -hex 32 生成>
APT_GY_ID=APT-REV01-001
APT_HTTP_PORT=8080
APT_PROBE_ENABLED=true
APT_PROBE_TIMEOUT_MS=2000
APT_DRUID_CONSOLE=false
APT_DRUID_USER=aptadmin
APT_DRUID_PASSWORD=
+13
View File
@@ -54,6 +54,13 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- IT-0 / IR-04:测试骨架(JUnit5 + MockMvc + Mockito) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -72,6 +79,12 @@
</execution>
</executions>
</plugin>
<!-- IT-0 / IR-04:JUnit5 需 Surefire 3.x,默认 2.12.4 会静默漏跑全部用例 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
@@ -0,0 +1,174 @@
package com.ruoyi.apt.ops.controller;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
import com.ruoyi.apt.framework.config.AptProperties;
import com.ruoyi.apt.infra.connectivity.AptConnectivityService;
import com.ruoyi.apt.infra.connectivity.ProbeResult;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import jakarta.servlet.http.HttpServletRequest;
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.RestController;
/**
* IT-0 基座自检接口(运维自检,非业务 Controller)。
*
* <p>按 R-02「冻结前不写 Controller」,本轮不提供任何入住 / 叫乘 / 账务业务接口;
* 本接口只用于验证数据库与 Redis 的连接、读写与异常链路。匿名访问受双重限制:
* 必须 apt.probe.enabled=true 且来源为回环地址。</p>
*
* @author apt-rev01
*/
@RestController
@RequestMapping(value = "/apt/ops/connectivity", produces = "application/json;charset=UTF-8")
public class AptConnectivityController
{
private final AptConnectivityService connectivityService;
private final AptProperties properties;
public AptConnectivityController(AptConnectivityService connectivityService,
AptProperties properties)
{
this.connectivityService = connectivityService;
this.properties = properties;
}
/**
* GET /apt/ops/connectivity —— 只读连通性检查(SELECT 1 + Redis PING + database 校验)。
* 请求参数:无。
*/
@Anonymous
@GetMapping
public AjaxResult check(HttpServletRequest request)
{
assertAllowed(request);
List<ProbeResult> results = new ArrayList<>();
results.add(connectivityService.checkDb());
results.add(connectivityService.checkRedis());
return assemble(results, true);
}
/**
* POST /apt/ops/connectivity/probe —— 读写闭环自检。
* 请求体:{ "target": "DB|REDIS|ALL", "strict": true }
* strict=true(默认):任一依赖失败即抛出统一错误码;strict=false 返回结构化降级结果。
*/
@Anonymous
@PostMapping("/probe")
public AjaxResult probe(@RequestBody(required = false) ProbeRequest body,
HttpServletRequest request)
{
assertAllowed(request);
ProbeRequest req = (body == null) ? new ProbeRequest() : body;
String target = (req.getTarget() == null) ? "ALL" : req.getTarget().trim().toUpperCase();
if (!"DB".equals(target) && !"REDIS".equals(target) && !"ALL".equals(target))
{
throw new AptException(AptErrorCode.BAD_REQUEST, "INVALID_TARGET",
"target 仅支持 DB / REDIS / ALL");
}
List<ProbeResult> results = new ArrayList<>();
if ("DB".equals(target) || "ALL".equals(target))
{
results.add(connectivityService.probeDb());
}
if ("REDIS".equals(target) || "ALL".equals(target))
{
results.add(connectivityService.probeRedis());
}
return assemble(results, req.isStrict());
}
private AjaxResult assemble(List<ProbeResult> results, boolean strict)
{
boolean allOk = true;
boolean anyOk = false;
long totalMs = 0;
List<Map<String, Object>> resultMaps = new ArrayList<>();
ProbeResult firstFailure = null;
for (ProbeResult result : results)
{
allOk &= result.isOk();
anyOk |= result.isOk();
totalMs += result.getElapsedMs();
resultMaps.add(result.toMap());
if (!result.isOk() && firstFailure == null)
{
firstFailure = result;
}
}
if (strict && firstFailure != null)
{
throw new AptException(AptErrorCode.fromCode(firstFailure.getCode()),
firstFailure.getReason(), "连通性自检失败:" + firstFailure.getTarget());
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("overall", allOk ? "OK" : (anyOk ? "DEGRADED" : "FAILED"));
data.put("gyid", AptContext.getGyId());
data.put("traceId", AptContext.getTraceId());
data.put("elapsedMs", totalMs);
data.put("results", resultMaps);
return new AjaxResult(AptErrorCode.SUCCESS.getCode(), "success", data);
}
private void assertAllowed(HttpServletRequest request)
{
if (!properties.getProbe().isEnabled())
{
throw new AptException(AptErrorCode.FORBIDDEN_PERM, "PROBE_DISABLED",
"连通性自检接口已关闭");
}
String remote = request.getRemoteAddr();
if (!isLoopback(remote))
{
throw new AptException(AptErrorCode.FORBIDDEN_PERM, "NON_LOOPBACK_FORBIDDEN",
"自检接口仅允许本机回环地址访问");
}
}
private boolean isLoopback(String remote)
{
if (remote == null)
{
return false;
}
return remote.startsWith("127.")
|| "0:0:0:0:0:0:0:1".equals(remote)
|| "::1".equals(remote);
}
/** POST /probe 请求体 */
public static class ProbeRequest
{
private String target = "ALL";
private boolean strict = true;
public String getTarget()
{
return target;
}
public void setTarget(String target)
{
this.target = target;
}
public boolean isStrict()
{
return strict;
}
public void setStrict(boolean strict)
{
this.strict = strict;
}
}
}
@@ -1,61 +1,54 @@
# 数据源配置
# 数据源配置 —— IT-0 第一轮(apt_rev01)
# 凭据一律外置到环境变量(IR-05 / 表 13-10 验收②:仓库内不得出现口令)
# 缺失 APT_DB_PASSWORD 时应用启动即失败(fail-fast),不回退到任何默认口令
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 从库数据源
url: jdbc:mysql://${APT_DB_HOST:127.0.0.1}:${APT_DB_PORT:3306}/${APT_DB_NAME:apt_rev01}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&connectTimeout=5000&socketTimeout=30000&rewriteBatchedStatements=true
username: ${APT_DB_USER:iotlocks}
password: ${APT_DB_PASSWORD}
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
url:
username:
password:
# ---- 连接池与超时(连接失败 / 超时策略)----
initialSize: 2
minIdle: 2
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
# 取连接等待上限:5s 快速失败,避免线程长期挂起
maxWait: 5000
connectTimeout: 5000
socketTimeout: 30000
# 连接有效性校验:借出时校验,坏连接立即被剔除
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnBorrow: true
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
timeBetweenEvictionRunsMillis: 30000
minEvictableIdleTimeMillis: 60000
maxEvictableIdleTimeMillis: 900000
# 连接初始化字符集
connection-init-sqls: SET NAMES utf8mb4
# 慢 SQL 与防火墙
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
multi-statement-allow: false
webStatFilter:
enabled: false
statViewServlet:
# 控制台默认关闭;确需开启时由环境变量提供口令,不落仓库
enabled: ${APT_DRUID_CONSOLE:false}
allow: 127.0.0.1
url-pattern: /druid/*
login-username: ${APT_DRUID_USER:aptadmin}
login-password: ${APT_DRUID_PASSWORD:}
@@ -1,122 +1,88 @@
# 项目相关配置
ruoyi:
# 名称
name: RuoYi
# 版本
name: AptRev01
version: 3.9.2
# 版权年份
copyrightYear: 2026
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
# 相对路径,避免硬编码本机绝对路径
profile: ${APT_UPLOAD_PATH:./uploadPath}
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口,默认为8080
port: 8080
port: ${APT_HTTP_PORT:8080}
servlet:
# 应用的访问路径
context-path: /
encoding:
charset: UTF-8
enabled: true
force-response: true
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数,默认为100
accept-count: 1000
threads:
# tomcat最大线程数,默认为200
max: 800
# Tomcat启动初始化的线程数,默认值10
min-spare: 100
# 日志配置
logging:
level:
com.ruoyi: debug
com.ruoyi.apt: info
org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间(默认10分钟)
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
enabled: false
data:
# redis 配置
redis:
# 地址
host: localhost
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
host: ${APT_REDIS_HOST:127.0.0.1}
port: ${APT_REDIS_PORT:6379}
# 第一轮要求:使用本机 Redis 的 database 1
database: ${APT_REDIS_DB:1}
password: ${APT_REDIS_PASSWORD:}
# 读写/连接超时 3s,超时快速失败并交由统一异常处理映射
timeout: 3s
connect-timeout: 3s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
max-active: 16
max-wait: 2s
shutdown-timeout: 100ms
# token配置
# token 密钥外置,不提供任何默认值
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期(默认30分钟)
secret: ${APT_TOKEN_SECRET}
expireTime: 30
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# PageHelper分页插件
pagehelper:
helperDialect: mysql
supportMethodsArguments: true
params: count=countSql
# Springdoc配置
springdoc:
api-docs:
path: /v3/api-docs
@@ -125,23 +91,32 @@ springdoc:
path: /swagger-ui.html
tags-sorter: alpha
group-configs:
- group: 'default'
display-name: '测试模块'
paths-to-match: '/**'
packages-to-scan: com.ruoyi.web.controller.tool
- group: 'apt'
display-name: '公寓端基座'
paths-to-match: '/apt/**'
packages-to-scan: com.ruoyi.apt.**
# 防盗链配置
referer:
# 防盗链开关
enabled: false
# 允许的域名列表
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
allowed-domains: localhost,127.0.0.1
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# ---- 公寓端基座自定义配置(IT-0 / IT-1)----
apt:
# 一进程一 gyid(D5):只从部署环境注入,不接受请求参数
gy-id: ${APT_GY_ID:APT-REV01-001}
probe:
enabled: ${APT_PROBE_ENABLED:true}
# 单次探针超时(毫秒),超过即判定 TIMEOUT
timeout-ms: ${APT_PROBE_TIMEOUT_MS:2000}
redis-key-prefix: ${APT_REDIS_KEY_PREFIX:apt:rev01:}
# 探针数据存活时间(秒),避免残留脏数据
redis-ttl-seconds: 60
# 探针表名(只写探针专用表,不触碰任何业务表)
table: apt_ops_probe
page:
max-size: 100
@@ -0,0 +1,47 @@
package com.ruoyi.apt.common;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
/** D5 gyid 隔离与 traceId 上下文的基线用例(不依赖数据库/Redis)。 */
class AptContextTest
{
@AfterEach
void tearDown()
{
AptContext.clear();
}
@Test
void missingGyIdIsRejected()
{
AptException e = assertThrows(AptException.class, AptContext::getGyId);
assertEquals(40302, e.getErrorCode().getCode());
assertEquals("GY_ID_MISSING", e.getReason());
}
@Test
void gyIdAndTraceIdAreStableWithinThread()
{
AptContext.setGyId("APT-REV01-001");
AptContext.initTraceId();
assertEquals("APT-REV01-001", AptContext.getGyId());
String traceId = AptContext.getTraceId();
assertNotNull(traceId);
assertEquals(traceId, AptContext.getTraceId());
}
@Test
void clearRemovesAllThreadLocals()
{
AptContext.setGyId("APT-REV01-001");
AptContext.initTraceId();
AptContext.clear();
assertNull(AptContext.getGyIdOrNull());
assertThrows(AptException.class, AptContext::getGyId);
}
}
@@ -0,0 +1,61 @@
package com.ruoyi.apt.framework;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import java.util.Map;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
import com.ruoyi.apt.framework.web.AptInfraExceptionHandler;
import com.ruoyi.common.core.domain.AjaxResult;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
/** 连接失败 / 超时 / 数据异常 -> 统一错误码映射的基线用例。 */
class AptInfraExceptionHandlerTest
{
private final AptInfraExceptionHandler handler = new AptInfraExceptionHandler();
private final HttpServletRequest request = mock(HttpServletRequest.class);
@Test
void dbConnectFailureMapsTo50301()
{
AjaxResult result = handler.handleDbConnectFailure(
new CannotGetJdbcConnectionException("connection refused"), request);
assertError(result, 50301, "DB_CONNECT_FAILED");
}
@Test
void timeoutMapsTo50301()
{
AjaxResult result = handler.handleTimeout(new QueryTimeoutException("slow query"), request);
assertError(result, 50301, "DEPENDENCY_TIMEOUT");
}
@Test
void duplicateKeyMapsTo40901()
{
AjaxResult result = handler.handleDuplicateKey(new DuplicateKeyException("dup"), request);
assertError(result, 40901, "DUPLICATE_KEY");
}
@Test
void aptExceptionKeepsCodeAndReason()
{
AjaxResult result = handler.handleAptException(
new AptException(AptErrorCode.DATA_ANOMALY, "SCHEMA_MISMATCH", "表结构不匹配"), request);
assertError(result, 50304, "SCHEMA_MISMATCH");
}
@SuppressWarnings("unchecked")
private void assertError(AjaxResult result, int code, String reason)
{
assertEquals(code, result.get(AjaxResult.CODE_TAG));
Map<String, Object> data = (Map<String, Object>) result.get(AjaxResult.DATA_TAG);
assertEquals(reason, data.get("reason"));
assertNotNull(data.get("traceId"), "错误响应必须带 traceId");
}
}
@@ -0,0 +1,116 @@
package com.ruoyi.apt.ops;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.framework.config.AptProperties;
import com.ruoyi.apt.framework.web.AptInfraExceptionHandler;
import com.ruoyi.apt.infra.connectivity.AptConnectivityService;
import com.ruoyi.apt.infra.connectivity.ProbeResult;
import com.ruoyi.apt.ops.controller.AptConnectivityController;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
/** 自检接口的请求参数、返回结构与错误码约定回归基线(不依赖真实 DB/Redis)。 */
@ExtendWith(MockitoExtension.class)
class AptConnectivityControllerTest
{
@Mock
private AptConnectivityService connectivityService;
private MockMvc mockMvc;
@BeforeEach
void setUp()
{
AptProperties properties = new AptProperties();
properties.setGyId("APT-REV01-001");
AptConnectivityController controller =
new AptConnectivityController(connectivityService, properties);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setControllerAdvice(new AptInfraExceptionHandler())
.build();
AptContext.setGyId("APT-REV01-001");
AptContext.initTraceId();
}
@AfterEach
void tearDown()
{
AptContext.clear();
}
@Test
void checkReturnsOverallOk() throws Exception
{
when(connectivityService.checkDb()).thenReturn(new ProbeResult("DB"));
when(connectivityService.checkRedis()).thenReturn(new ProbeResult("REDIS"));
mockMvc.perform(get("/apt/ops/connectivity"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.overall").value("OK"))
.andExpect(jsonPath("$.data.gyid").value("APT-REV01-001"))
.andExpect(jsonPath("$.data.results.length()").value(2))
.andExpect(jsonPath("$.data.traceId").exists());
}
@Test
void invalidTargetReturns40001() throws Exception
{
mockMvc.perform(post("/apt/ops/connectivity/probe")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"target\":\"UNKNOWN\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(40001))
.andExpect(jsonPath("$.data.reason").value("INVALID_TARGET"));
}
@Test
void strictFailureReturns50301() throws Exception
{
when(connectivityService.probeDb()).thenReturn(new ProbeResult("DB")
.fail(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DB_CONNECT_FAILED", "connection refused"));
mockMvc.perform(post("/apt/ops/connectivity/probe")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"target\":\"DB\",\"strict\":true}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(50301))
.andExpect(jsonPath("$.data.reason").value("DB_CONNECT_FAILED"));
}
@Test
void nonStrictFailureReturnsDegradedStructure() throws Exception
{
when(connectivityService.probeDb()).thenReturn(new ProbeResult("DB"));
when(connectivityService.probeRedis()).thenReturn(new ProbeResult("REDIS")
.fail(AptErrorCode.DEPENDENCY_UNAVAILABLE, "REDIS_TIMEOUT", "timeout"));
mockMvc.perform(post("/apt/ops/connectivity/probe")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"target\":\"ALL\",\"strict\":false}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.overall").value("DEGRADED"))
.andExpect(jsonPath("$.data.results[1].reason").value("REDIS_TIMEOUT"));
}
@Test
void nonLoopbackIsForbidden() throws Exception
{
mockMvc.perform(get("/apt/ops/connectivity")
.with(req -> { req.setRemoteAddr("10.0.0.9"); return req; }))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(40301))
.andExpect(jsonPath("$.data.reason").value("NON_LOOPBACK_FORBIDDEN"));
}
}
@@ -0,0 +1,65 @@
package com.ruoyi.apt.common;
import java.util.UUID;
/**
* 公寓端运行时上下文:gyid 与 traceId。
*
* <p>D5「一进程一 gyid」:gyid 只从部署环境注入(拦截器写入),
* 绝不接受请求参数;所有业务 SQL 必须带 gyid。traceId 用于错误响应
* 与日志关联,避免把堆栈外泄给调用方。</p>
*
* @author apt-rev01
*/
public final class AptContext
{
private static final ThreadLocal<String> GY_ID = new ThreadLocal<>();
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
private AptContext()
{
}
public static void setGyId(String gyId)
{
GY_ID.set(gyId);
}
/** 取 gyid;未注入视为隔离失效,直接拒绝(40302) */
public static String getGyId()
{
String value = GY_ID.get();
if (value == null || value.isEmpty())
{
throw new AptException(AptErrorCode.FORBIDDEN_GYID, "GY_ID_MISSING", "公寓标识未注入");
}
return value;
}
public static String getGyIdOrNull()
{
return GY_ID.get();
}
public static void initTraceId()
{
TRACE_ID.set(UUID.randomUUID().toString().replace("-", ""));
}
public static String getTraceId()
{
String value = TRACE_ID.get();
if (value == null)
{
value = UUID.randomUUID().toString().replace("-", "");
TRACE_ID.set(value);
}
return value;
}
public static void clear()
{
GY_ID.remove();
TRACE_ID.remove();
}
}
@@ -0,0 +1,55 @@
package com.ruoyi.apt.common;
/**
* 公寓端统一错误码(沿用 公寓端/04 表 4-4 分段,不新造号段)。
*
* <p>第一轮新增 50304「数据异常」落在既有 503xx 段内(与 CR-02 在同一分段内
* 追加 50303 的做法一致),需阶段二契约冻结评审登记后方可对外启用。</p>
*
* @author apt-rev01
*/
public enum AptErrorCode
{
SUCCESS(200, "success"),
BAD_REQUEST(40001, "参数错误"),
UNAUTHORIZED(40101, "未登录"),
FORBIDDEN_PERM(40301, "缺少权限点"),
FORBIDDEN_GYID(40302, "gyid 越界或缺失"),
NOT_FOUND(40401, "资源不存在"),
DUPLICATE_KEY(40901, "业务键重复"),
DEPENDENCY_UNAVAILABLE(50301, "依赖不可用"),
/** 数据异常:完整性约束、读写结果不符、序列化失败 */
DATA_ANOMALY(50304, "数据异常"),
INTERNAL_ERROR(500, "内部错误");
private final int code;
private final String message;
AptErrorCode(int code, String message)
{
this.code = code;
this.message = message;
}
public int getCode()
{
return code;
}
public String getMessage()
{
return message;
}
public static AptErrorCode fromCode(int code)
{
for (AptErrorCode value : values())
{
if (value.code == code)
{
return value;
}
}
return INTERNAL_ERROR;
}
}
@@ -0,0 +1,38 @@
package com.ruoyi.apt.common;
/**
* 带错误码与 reason 的公寓端异常,由统一异常处理器映射为 HTTP 响应。
*
* @author apt-rev01
*/
public class AptException extends RuntimeException
{
private static final long serialVersionUID = 1L;
private final AptErrorCode errorCode;
private final String reason;
public AptException(AptErrorCode errorCode, String reason, String message)
{
super(message);
this.errorCode = errorCode;
this.reason = reason;
}
public AptException(AptErrorCode errorCode, String reason, String message, Throwable cause)
{
super(message, cause);
this.errorCode = errorCode;
this.reason = reason;
}
public AptErrorCode getErrorCode()
{
return errorCode;
}
public String getReason()
{
return reason;
}
}
@@ -0,0 +1,126 @@
package com.ruoyi.apt.framework.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 公寓端基座配置(application.yml 的 apt.* 段)。
*
* @author apt-rev01
*/
@Component
@ConfigurationProperties(prefix = "apt")
public class AptProperties
{
/** 一进程一 gyid(D5),只从部署环境注入 */
private String gyId;
private Probe probe = new Probe();
private Page page = new Page();
public String getGyId()
{
return gyId;
}
public void setGyId(String gyId)
{
this.gyId = gyId;
}
public Probe getProbe()
{
return probe;
}
public void setProbe(Probe probe)
{
this.probe = probe;
}
public Page getPage()
{
return page;
}
public void setPage(Page page)
{
this.page = page;
}
public static class Probe
{
private boolean enabled = true;
/** 单次探针超时判定阈值(毫秒) */
private long timeoutMs = 2000;
private String redisKeyPrefix = "apt:rev01:";
private int redisTtlSeconds = 60;
/** 探针专用表,不触碰任何业务表 */
private String table = "apt_ops_probe";
public boolean isEnabled()
{
return enabled;
}
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public long getTimeoutMs()
{
return timeoutMs;
}
public void setTimeoutMs(long timeoutMs)
{
this.timeoutMs = timeoutMs;
}
public String getRedisKeyPrefix()
{
return redisKeyPrefix;
}
public void setRedisKeyPrefix(String redisKeyPrefix)
{
this.redisKeyPrefix = redisKeyPrefix;
}
public int getRedisTtlSeconds()
{
return redisTtlSeconds;
}
public void setRedisTtlSeconds(int redisTtlSeconds)
{
this.redisTtlSeconds = redisTtlSeconds;
}
public String getTable()
{
return table;
}
public void setTable(String table)
{
this.table = table;
}
}
public static class Page
{
private int maxSize = 100;
public int getMaxSize()
{
return maxSize;
}
public void setMaxSize(int maxSize)
{
this.maxSize = maxSize;
}
}
}
@@ -0,0 +1,40 @@
package com.ruoyi.apt.framework.web;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.framework.config.AptProperties;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 把部署环境注入的 gyid 与 traceId 放入线程上下文。
* 仅作用于 /apt/**,且只从配置读取,绝不采信请求参数。
*
* @author apt-rev01
*/
@Component
public class AptContextInterceptor implements HandlerInterceptor
{
private final AptProperties properties;
public AptContextInterceptor(AptProperties properties)
{
this.properties = properties;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
{
AptContext.initTraceId();
AptContext.setGyId(properties.getGyId());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex)
{
AptContext.clear();
}
}
@@ -0,0 +1,111 @@
package com.ruoyi.apt.framework.web;
import java.util.LinkedHashMap;
import java.util.Map;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
import com.ruoyi.common.core.domain.AjaxResult;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 基础设施异常统一映射:连接失败 / 超时 / 数据异常 -> 约定错误码 + reason + traceId。
* 不向调用方外泄堆栈,只写日志(公寓端/04 表 4-4、表 13-11 M0 验收)。
*
* @author apt-rev01
*/
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AptInfraExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(AptInfraExceptionHandler.class);
@ExceptionHandler(AptException.class)
public AjaxResult handleAptException(AptException e, HttpServletRequest request)
{
log.error("apt infra error, uri={}, code={}, reason={}, traceId={}",
request.getRequestURI(), e.getErrorCode().getCode(), e.getReason(),
AptContext.getTraceId(), e);
return build(e.getErrorCode(), e.getReason(), e.getMessage(), request);
}
@ExceptionHandler(DuplicateKeyException.class)
public AjaxResult handleDuplicateKey(DuplicateKeyException e, HttpServletRequest request)
{
return handle(AptErrorCode.DUPLICATE_KEY, "DUPLICATE_KEY", "业务键重复", e, request);
}
@ExceptionHandler(CannotGetJdbcConnectionException.class)
public AjaxResult handleDbConnectFailure(CannotGetJdbcConnectionException e, HttpServletRequest request)
{
return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DB_CONNECT_FAILED", "数据库连接失败", e, request);
}
@ExceptionHandler(QueryTimeoutException.class)
public AjaxResult handleTimeout(QueryTimeoutException e, HttpServletRequest request)
{
return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DEPENDENCY_TIMEOUT", "依赖访问超时", e, request);
}
@ExceptionHandler(BadSqlGrammarException.class)
public AjaxResult handleSchemaMismatch(BadSqlGrammarException e, HttpServletRequest request)
{
return handle(AptErrorCode.DATA_ANOMALY, "SCHEMA_MISMATCH", "表结构与代码不匹配", e, request);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public AjaxResult handleIntegrity(DataIntegrityViolationException e, HttpServletRequest request)
{
return handle(AptErrorCode.DATA_ANOMALY, "DATA_INTEGRITY_VIOLATION", "数据完整性约束被破坏", e, request);
}
@ExceptionHandler(RedisConnectionFailureException.class)
public AjaxResult handleRedisConnectFailure(RedisConnectionFailureException e, HttpServletRequest request)
{
return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "REDIS_CONNECT_FAILED", "Redis 连接失败", e, request);
}
@ExceptionHandler(RedisSystemException.class)
public AjaxResult handleRedisSystem(RedisSystemException e, HttpServletRequest request)
{
return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "REDIS_ERROR", "Redis 命令执行异常", e, request);
}
/** JDBC / Redis 的兜底数据访问异常 */
@ExceptionHandler(DataAccessException.class)
public AjaxResult handleDataAccess(DataAccessException e, HttpServletRequest request)
{
return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DEPENDENCY_ERROR", "依赖访问异常", e, request);
}
private AjaxResult handle(AptErrorCode code, String reason, String message,
Exception e, HttpServletRequest request)
{
log.error("apt infra error, uri={}, code={}, reason={}, traceId={}",
request.getRequestURI(), code.getCode(), reason, AptContext.getTraceId(), e);
return build(code, reason, message, request);
}
private AjaxResult build(AptErrorCode code, String reason, String message, HttpServletRequest request)
{
Map<String, Object> data = new LinkedHashMap<>();
data.put("reason", reason);
data.put("traceId", AptContext.getTraceId());
data.put("path", request.getRequestURI());
return new AjaxResult(code.getCode(), message, data);
}
}
@@ -0,0 +1,27 @@
package com.ruoyi.apt.framework.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* /apt/** 前缀的基座拦截器注册(四前缀拦截器最小集,IT-1 再补 /duty/** 与 /open/**)。
*
* @author apt-rev01
*/
@Configuration
public class AptWebMvcConfig implements WebMvcConfigurer
{
private final AptContextInterceptor aptContextInterceptor;
public AptWebMvcConfig(AptContextInterceptor aptContextInterceptor)
{
this.aptContextInterceptor = aptContextInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(aptContextInterceptor).addPathPatterns("/apt/**");
}
}
@@ -0,0 +1,349 @@
package com.ruoyi.apt.infra.connectivity;
import java.time.Duration;
import java.util.UUID;
import javax.sql.DataSource;
import com.ruoyi.apt.common.AptContext;
import com.ruoyi.apt.common.AptErrorCode;
import com.ruoyi.apt.common.AptException;
import com.ruoyi.apt.framework.config.AptProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* 数据库 / Redis 连通性与读写闭环探针(IT-0 基座,不触碰任何业务表)。
*
* <p>异常分类:连接失败 -> 50301 + DB_CONNECT_FAILED / REDIS_CONNECT_FAILED;
* 超时 -> 50301 + *_TIMEOUT;重复键 -> 40901;完整性 / 读写结果不符 -> 50304。
* JDBC 查询超时由 JdbcTemplate#setQueryTimeout 从 apt.probe.timeout-ms 推导,
* Redis 命令超时由 spring.data.redis.timeout 统一兜底,探针再按 elapsedMs 二次判定。</p>
*
* @author apt-rev01
*/
@Service
public class AptConnectivityService
{
private static final Logger log = LoggerFactory.getLogger(AptConnectivityService.class);
private static final String PROBE_VALUE = "it0-probe";
private final JdbcTemplate jdbc;
private final StringRedisTemplate redis;
private final AptProperties properties;
public AptConnectivityService(DataSource dataSource, StringRedisTemplate redis,
AptProperties properties)
{
this.jdbc = new JdbcTemplate(dataSource);
int timeoutSeconds = (int) Math.max(1, properties.getProbe().getTimeoutMs() / 1000);
this.jdbc.setQueryTimeout(timeoutSeconds);
this.redis = redis;
this.properties = properties;
}
/** 只读连通性检查:SELECT 1 + 探针表计数 */
public ProbeResult checkDb()
{
ProbeResult result = new ProbeResult("DB");
long start = System.nanoTime();
try
{
Integer one = jdbc.queryForObject("SELECT 1", Integer.class);
if (one == null || one != 1)
{
return fail(result, AptErrorCode.DATA_ANOMALY, "DB_READ_MISMATCH", "SELECT 1 返回值异常");
}
Long rows = jdbc.queryForObject("SELECT COUNT(*) FROM " + probeTable(), Long.class);
result.detail("operation", "SELECT");
result.detail("select1", one);
result.detail("probeRows", rows);
result.detail("gyid", gyId());
}
catch (CannotGetJdbcConnectionException e)
{
classifyDb(result, "DB_CONNECT_FAILED", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
catch (QueryTimeoutException e)
{
classifyDb(result, "DB_TIMEOUT", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
catch (BadSqlGrammarException e)
{
classifyDb(result, "SCHEMA_MISMATCH", AptErrorCode.DATA_ANOMALY, e);
}
catch (DataIntegrityViolationException e)
{
classifyDb(result, "DATA_INTEGRITY_VIOLATION", AptErrorCode.DATA_ANOMALY, e);
}
catch (DataAccessException e)
{
classifyDb(result, "DB_ERROR", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
finally
{
finish(result, start);
}
return result;
}
/** 读写闭环:INSERT -> SELECT -> DELETE,失败不残留脏数据 */
public ProbeResult probeDb()
{
ProbeResult result = new ProbeResult("DB");
String key = "db-" + shortId();
String table = probeTable();
long start = System.nanoTime();
try
{
int inserted = jdbc.update(
"INSERT INTO " + table + " (gyid, probe_key, payload) VALUES (?, ?, ?)",
gyId(), key, PROBE_VALUE);
String read = jdbc.queryForObject(
"SELECT payload FROM " + table + " WHERE gyid = ? AND probe_key = ?",
String.class, gyId(), key);
int deleted = jdbc.update(
"DELETE FROM " + table + " WHERE gyid = ? AND probe_key = ?", gyId(), key);
if (inserted != 1 || !PROBE_VALUE.equals(read) || deleted != 1)
{
return fail(result, AptErrorCode.DATA_ANOMALY, "DB_READ_WRITE_MISMATCH",
"写入/读取/删除结果不一致");
}
result.detail("operation", "INSERT/SELECT/DELETE");
result.detail("inserted", inserted);
result.detail("readBack", read);
result.detail("deleted", deleted);
result.detail("gyid", gyId());
}
catch (CannotGetJdbcConnectionException e)
{
classifyDb(result, "DB_CONNECT_FAILED", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
catch (QueryTimeoutException e)
{
classifyDb(result, "DB_TIMEOUT", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
catch (DuplicateKeyException e)
{
classifyDb(result, "DUPLICATE_KEY", AptErrorCode.DUPLICATE_KEY, e);
}
catch (BadSqlGrammarException e)
{
classifyDb(result, "SCHEMA_MISMATCH", AptErrorCode.DATA_ANOMALY, e);
}
catch (DataIntegrityViolationException e)
{
classifyDb(result, "DATA_INTEGRITY_VIOLATION", AptErrorCode.DATA_ANOMALY, e);
}
catch (DataAccessException e)
{
classifyDb(result, "DB_ERROR", AptErrorCode.DEPENDENCY_UNAVAILABLE, e);
}
finally
{
cleanupDbQuietly(table, key);
finish(result, start);
}
return result;
}
/** 只读连通性检查:PING + database 索引校验 */
public ProbeResult checkRedis()
{
ProbeResult result = new ProbeResult("REDIS");
long start = System.nanoTime();
try
{
String pong = redis.execute((RedisCallback<String>) connection -> connection.ping());
int database = redisDatabase();
if (pong == null || !"PONG".equalsIgnoreCase(pong))
{
return fail(result, AptErrorCode.DATA_ANOMALY, "REDIS_READ_MISMATCH", "PING 返回值异常");
}
if (database != 1)
{
return fail(result, AptErrorCode.DATA_ANOMALY, "REDIS_DB_MISMATCH",
"Redis database 应为 1,实际为 " + database);
}
result.detail("operation", "PING");
result.detail("ping", pong);
result.detail("database", database);
}
catch (RedisConnectionFailureException e)
{
classifyRedis(result, "REDIS_CONNECT_FAILED", e);
}
catch (QueryTimeoutException e)
{
classifyRedis(result, "REDIS_TIMEOUT", e);
}
catch (RedisSystemException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
catch (DataAccessException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
catch (RuntimeException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
finally
{
finish(result, start);
}
return result;
}
/** 读写闭环:SET(带 TTL) -> GET -> DEL,失败不残留 key */
public ProbeResult probeRedis()
{
ProbeResult result = new ProbeResult("REDIS");
String key = properties.getProbe().getRedisKeyPrefix() + "probe:" + shortId();
long start = System.nanoTime();
try
{
redis.opsForValue().set(key, PROBE_VALUE,
Duration.ofSeconds(Math.max(1, properties.getProbe().getRedisTtlSeconds())));
String read = redis.opsForValue().get(key);
Boolean deleted = redis.delete(key);
if (!PROBE_VALUE.equals(read) || !Boolean.TRUE.equals(deleted))
{
return fail(result, AptErrorCode.DATA_ANOMALY, "REDIS_READ_WRITE_MISMATCH",
"SET/GET/DEL 结果不一致");
}
result.detail("operation", "SET/GET/DEL");
result.detail("key", key);
result.detail("readBack", read);
result.detail("deleted", deleted);
result.detail("database", redisDatabase());
result.detail("ttlSeconds", properties.getProbe().getRedisTtlSeconds());
}
catch (RedisConnectionFailureException e)
{
classifyRedis(result, "REDIS_CONNECT_FAILED", e);
}
catch (QueryTimeoutException e)
{
classifyRedis(result, "REDIS_TIMEOUT", e);
}
catch (RedisSystemException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
catch (DataAccessException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
catch (RuntimeException e)
{
classifyRedis(result, "REDIS_ERROR", e);
}
finally
{
cleanupRedisQuietly(key);
finish(result, start);
}
return result;
}
private void classifyDb(ProbeResult result, String reason, AptErrorCode code, Exception e)
{
log.warn("db probe failed, reason={}, traceId={}, cause={}", reason,
AptContext.getTraceId(), e.toString());
result.fail(code, reason, e.getClass().getSimpleName());
}
private void classifyRedis(ProbeResult result, String reason, Exception e)
{
log.warn("redis probe failed, reason={}, traceId={}, cause={}", reason,
AptContext.getTraceId(), e.toString());
result.fail(AptErrorCode.DEPENDENCY_UNAVAILABLE, reason, e.getClass().getSimpleName());
}
private ProbeResult fail(ProbeResult result, AptErrorCode code, String reason, String message)
{
result.fail(code, reason, message);
return result;
}
/** 超过配置阈值但底层未报超时时,仍按超时归类 */
private void finish(ProbeResult result, long startNanos)
{
result.elapsed(startNanos);
if (result.isOk() && result.getElapsedMs() > properties.getProbe().getTimeoutMs())
{
result.fail(AptErrorCode.DEPENDENCY_UNAVAILABLE,
result.getTarget() + "_TIMEOUT", "探针耗时超过阈值");
}
}
private void cleanupDbQuietly(String table, String key)
{
try
{
jdbc.update("DELETE FROM " + table + " WHERE gyid = ? AND probe_key = ?", gyId(), key);
}
catch (Exception e)
{
log.warn("db probe cleanup skipped, traceId={}, cause={}", AptContext.getTraceId(), e.toString());
}
}
private void cleanupRedisQuietly(String key)
{
try
{
redis.delete(key);
}
catch (Exception e)
{
log.warn("redis probe cleanup skipped, traceId={}, cause={}", AptContext.getTraceId(), e.toString());
}
}
private int redisDatabase()
{
RedisConnectionFactory factory = redis.getConnectionFactory();
if (factory instanceof LettuceConnectionFactory)
{
return ((LettuceConnectionFactory) factory).getDatabase();
}
return -1;
}
private String gyId()
{
String value = AptContext.getGyIdOrNull();
return (value == null || value.isEmpty()) ? properties.getGyId() : value;
}
/** 表名来自配置,做白名单校验,避免配置注入 */
private String probeTable()
{
String table = properties.getProbe().getTable();
if (table == null || !table.matches("[A-Za-z0-9_]+"))
{
throw new AptException(AptErrorCode.BAD_REQUEST, "INVALID_PROBE_TABLE", "探针表名非法");
}
return table;
}
private String shortId()
{
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
@@ -0,0 +1,96 @@
package com.ruoyi.apt.infra.connectivity;
import java.util.LinkedHashMap;
import java.util.Map;
import com.ruoyi.apt.common.AptErrorCode;
/**
* 单个依赖(DB / REDIS)的探针结果。
* 失败时不抛异常,先落成结构化结果;控制器按 strict 决定是否交给统一异常处理器。
*
* @author apt-rev01
*/
public class ProbeResult
{
private final String target;
private final Map<String, Object> details = new LinkedHashMap<>();
private boolean ok = true;
private int code = AptErrorCode.SUCCESS.getCode();
private String reason = "OK";
private String message = "ok";
private long elapsedMs;
public ProbeResult(String target)
{
this.target = target;
}
public String getTarget()
{
return target;
}
public boolean isOk()
{
return ok;
}
public int getCode()
{
return code;
}
public String getReason()
{
return reason;
}
public String getMessage()
{
return message;
}
public long getElapsedMs()
{
return elapsedMs;
}
public Map<String, Object> getDetails()
{
return details;
}
public ProbeResult detail(String key, Object value)
{
this.details.put(key, value);
return this;
}
public ProbeResult fail(AptErrorCode errorCode, String reason, String message)
{
this.ok = false;
this.code = errorCode.getCode();
this.reason = reason;
this.message = message;
return this;
}
public ProbeResult elapsed(long startNanos)
{
this.elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L;
return this;
}
public Map<String, Object> toMap()
{
Map<String, Object> map = new LinkedHashMap<>();
map.put("target", target);
map.put("ok", ok);
map.put("code", code);
map.put("reason", reason);
map.put("message", message);
map.put("elapsedMs", elapsedMs);
map.put("details", details);
return map;
}
}
+26
View File
@@ -0,0 +1,26 @@
# 本地启动脚本:从被 .gitignore 排除的 .env 加载凭据,仓库内不含任何真实口令。
# 用法:复制 dev/.env.example 为 dev/.env 并填写,然后执行本脚本。
$ErrorActionPreference = 'Stop'
$envFile = Join-Path $PSScriptRoot '.env'
if (-not (Test-Path $envFile)) {
throw "缺少 $envFile,请先从 .env.example 复制并填写本机凭据"
}
Get-Content -LiteralPath $envFile | ForEach-Object {
$line = $_.Trim()
if ($line -eq '' -or $line.StartsWith('#')) { return }
if ($line -match '^([A-Za-z0-9_]+)\s*=\s*(.*)$') {
[Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], 'Process')
}
}
foreach ($required in @('APT_DB_PASSWORD', 'APT_TOKEN_SECRET')) {
if (-not [Environment]::GetEnvironmentVariable($required, 'Process')) {
throw "缺少必需环境变量 $required(请在 .env 中填写)"
}
}
$backend = Join-Path (Split-Path $PSScriptRoot -Parent) 'RuoYi-Vue-master'
Set-Location $backend
& mvn -pl ruoyi-admin -am spring-boot:run
+50
View File
@@ -0,0 +1,50 @@
-- =============================================================================
-- IT-0 / DV-0 分域增量 DDL —— 公共部分(幂等辅助 + 连通性探针表)
-- 目标库:apt_rev01 @ 127.0.0.1:3306 / MySQL 9.5
-- 设计口径:CR-02 表 3-25(59 张)、表 3-33(PK/UK/IDX/迁移顺序)、
-- 公寓端/03 表 3-8~3-15、公寓端/13 表 13-10 验收③(二次执行幂等)
-- 通用规则:
-- 1) InnoDB / utf8mb4 / utf8mb4_0900_ai_ci
-- 2) 代理主键 id bigint AUTO_INCREMENT;业务幂等键一律 UK 且以 gyid 打头
-- 3) 不建外键(docs/10 表 5-6:跨端/分库场景用应用层约束 + 对账兜底)
-- 4) 状态用 varchar 大写下划线枚举 + CHECK,不用 MySQL ENUM、不用数字码
-- 5) 金额 decimal(12,2)、比率 decimal(6,4)、时间 datetime(3)、禁止浮点
-- 6) deleted tinyint NOT NULL DEFAULT 0;create_time/update_time 审计列
-- 7) 所有 CREATE TABLE 带 IF NOT EXISTS,可重复执行
-- =============================================================================
SET NAMES utf8mb4;
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
-- 幂等补列存储过程:供 07_patch_columns.sql 与后续迭代增量迁移使用
DROP PROCEDURE IF EXISTS apt_add_column_if_missing;
DELIMITER $$
CREATE PROCEDURE apt_add_column_if_missing(
IN p_table VARCHAR(64),
IN p_column VARCHAR(64),
IN p_ddl TEXT)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = p_table
AND COLUMN_NAME = p_column
) THEN
SET @apt_ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN ', p_ddl);
PREPARE apt_stmt FROM @apt_ddl;
EXECUTE apt_stmt;
DEALLOCATE PREPARE apt_stmt;
END IF;
END$$
DELIMITER ;
-- 连通性探针专用表(不计入 59 张业务表):只做 INSERT/SELECT/DELETE 闭环验证
CREATE TABLE IF NOT EXISTS apt_ops_probe (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '代理主键',
gyid VARCHAR(32) NOT NULL COMMENT '公寓隔离标识',
probe_key VARCHAR(64) NOT NULL COMMENT '探针幂等键',
payload VARCHAR(256) NULL COMMENT '探针载荷',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '写入时刻',
PRIMARY KEY (id),
UNIQUE KEY uk_probe_gyid_key (gyid, probe_key),
KEY idx_probe_gyid_time (gyid, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IT-0 连通性探针表(非业务表)';
+260
View File
@@ -0,0 +1,260 @@
-- =============================================================================
-- 域 1:主数据快照 + 计划任务 + 本地建单(11 张)
-- 来源:公寓端/03 表 3-1(7)、表 3-2(3)、CR-01 表 1-17(apt_local_task)
-- 列级定义:apt_local_task 见 CR-01 表 1-17 + 公寓端/11 表 11-7;
-- 其余 10 张文档只给「表名/owner/写入口」,字段按接口契约与
-- 公寓端/03 表 3-9/3-15 通用规则推断(标注 [推断]),待阶段二冻结评审。
-- =============================================================================
-- 1 主数据版本(platform-down,只读快照;Mock 时 source='LOCAL')
CREATE TABLE IF NOT EXISTS apt_master_version (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '代理主键',
gyid VARCHAR(32) NOT NULL COMMENT '公寓隔离标识',
domain VARCHAR(32) NOT NULL COMMENT '快照域 APARTMENT/ROOM/PERSON/ROUTE/RULE/HOLIDAY',
source_version BIGINT NOT NULL DEFAULT 0 COMMENT '平台版本号,单调递增',
source VARCHAR(20) NOT NULL DEFAULT 'PLATFORM' COMMENT 'PLATFORM / LOCAL(Mock 数据标 LOCAL)',
state VARCHAR(20) NOT NULL DEFAULT 'APPLIED' COMMENT 'PENDING/APPLIED/FAILED',
applied_at DATETIME(3) NULL COMMENT '生效时刻',
remark VARCHAR(256) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_mv_gyid_domain (gyid, domain),
KEY idx_mv_gyid_state (gyid, state, applied_at),
CONSTRAINT ck_mv_source CHECK (source IN ('PLATFORM','LOCAL')),
CONSTRAINT ck_mv_state CHECK (state IN ('PENDING','APPLIED','FAILED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='主数据版本快照 [推断]';
-- 2 公寓档案
CREATE TABLE IF NOT EXISTS apt_master_apartment (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
apartment_code VARCHAR(64) NOT NULL COMMENT '公寓编码',
apartment_name VARCHAR(128) NULL,
city_code VARCHAR(32) NULL,
address VARCHAR(256) NULL,
contact_ref VARCHAR(128) NULL COMMENT '联系方式只存引用',
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_ma_gyid_code (gyid, apartment_code),
CONSTRAINT ck_ma_state CHECK (state IN ('ACTIVE','DISABLED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='公寓档案 [推断]';
-- 3 房间主数据(含 CR-02 表 3-32 补列 building;Q-20 未决前列存在但不启用)
CREATE TABLE IF NOT EXISTS apt_master_room (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL COMMENT '房间主键(平台下发)',
room_no VARCHAR(32) NOT NULL COMMENT '房号',
room_floor VARCHAR(32) NULL COMMENT '楼层(Q-20 未决前的筛选落点)',
building VARCHAR(64) NULL COMMENT '楼栋;Q-20 未决,保留列不启用(表 3-32)',
bed_count INT NOT NULL DEFAULT 0,
orientation VARCHAR(32) NULL,
room_type VARCHAR(32) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
state_version BIGINT NOT NULL DEFAULT 1 COMMENT '房态乐观锁,与 source_version 语义不同',
source_version BIGINT NOT NULL DEFAULT 0 COMMENT '平台快照版本;改快照字段返回 40904',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_mr_gyid_room (gyid, room_id),
KEY idx_mr_gyid_floor_no (gyid, room_floor, room_no),
CONSTRAINT ck_mr_state CHECK (state IN ('ACTIVE','DISABLED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='房间主数据(platform-down 只读)[推断+补列]';
-- 4 人员主数据(含 公寓端/10 表 10-6 人脸补列:媒体只存引用与校验和)
CREATE TABLE IF NOT EXISTS apt_master_person (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
ryid VARCHAR(64) NOT NULL COMMENT '人员 ID',
xm VARCHAR(64) NOT NULL COMMENT '姓名(快照字段,改则返回 40904)',
work_no VARCHAR(64) NULL COMMENT '工号,平台侧校验唯一性',
dept_code VARCHAR(64) NULL,
person_category VARCHAR(20) NOT NULL DEFAULT 'TRANSIENT' COMMENT 'TRANSIENT / LONG_TERM',
photo_ref VARCHAR(512) NULL COMMENT '照片引用,不存原图二进制',
photo_checksum VARCHAR(128) NULL,
feature_ref VARCHAR(512) NULL COMMENT '特征值引用,不存特征大对象',
feature_checksum VARCHAR(128) NULL,
feature_algo VARCHAR(32) NULL,
feature_version BIGINT NOT NULL DEFAULT 1,
face_sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SYNCED/FAILED/STALE',
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_mp_gyid_ryid (gyid, ryid),
KEY idx_mp_gyid_workno (gyid, work_no),
KEY idx_mp_gyid_name (gyid, xm),
CONSTRAINT ck_mp_category CHECK (person_category IN ('TRANSIENT','LONG_TERM')),
CONSTRAINT ck_mp_face CHECK (face_sync_state IN ('PENDING','SYNCED','FAILED','STALE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='人员主数据(含 §12 补列)';
-- 5 交路主数据
CREATE TABLE IF NOT EXISTS apt_master_route (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
jlid VARCHAR(64) NOT NULL COMMENT '交路 ID;重复新增返回 40901',
cc VARCHAR(64) NOT NULL COMMENT '车次(必填,缺则 40001)',
kcsj DATETIME(3) NOT NULL COMMENT '开车时刻(必填)',
route_name VARCHAR(128) NULL,
start_station VARCHAR(64) NULL,
end_station VARCHAR(64) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
stop_reason VARCHAR(256) NULL COMMENT '停用原因(必填,审计留痕)',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_mroute_gyid_jlid (gyid, jlid),
KEY idx_mroute_gyid_cc_time (gyid, cc, kcsj),
CONSTRAINT ck_mroute_state CHECK (state IN ('ACTIVE','STOPPED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='交路主数据 [推断]';
-- 6 叫乘规则(规则体用 JSON,避免频繁改表)
CREATE TABLE IF NOT EXISTS apt_master_rule (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
rule_id VARCHAR(64) NOT NULL,
rule_type VARCHAR(32) NOT NULL COMMENT 'CALL_TIME / HOLIDAY / TIMEOUT',
rule_version BIGINT NOT NULL DEFAULT 1,
rule_body JSON NULL COMMENT '规则体 JSON',
effective_from DATETIME(3) NULL,
effective_to DATETIME(3) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_mrule_gyid_id_ver (gyid, rule_id, rule_version),
KEY idx_mrule_gyid_type (gyid, rule_type, state)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='叫乘规则快照 [推断]';
-- 7 节假日
CREATE TABLE IF NOT EXISTS apt_holiday (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
holiday_date DATE NOT NULL,
holiday_name VARCHAR(64) NULL,
holiday_type VARCHAR(20) NOT NULL DEFAULT 'HOLIDAY',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_holiday_gyid_date (gyid, holiday_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='节假日表 [推断]';
-- 8 平台计划下行落库(jcsj 仅基线,权威值恒由公寓复算 CR-C06)
CREATE TABLE IF NOT EXISTS gy_syn_jcxx (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
jcid VARCHAR(64) NOT NULL COMMENT '叫乘计划 ID',
jlid VARCHAR(64) NULL,
cc VARCHAR(64) NULL,
ryid VARCHAR(64) NULL,
xm VARCHAR(64) NULL,
fjh VARCHAR(32) NULL COMMENT '房间号',
kcsj DATETIME(3) NULL,
jcsj DATETIME(3) NULL COMMENT '平台计划基线(非权威)',
computed_jcsj DATETIME(3) NULL COMMENT '公寓复算值(权威)',
plan_version BIGINT NOT NULL DEFAULT 1,
sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SYNCED/FAILED/DRIFT',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_syn_gyid_jcid (gyid, jcid),
KEY idx_syn_gyid_jlid_time (gyid, jlid, jcsj),
CONSTRAINT ck_syn_state CHECK (sync_state IN ('PENDING','SYNCED','FAILED','DRIFT'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='平台计划下行 [推断]';
-- 9 计划同步状态
CREATE TABLE IF NOT EXISTS gy_syn_jcxx_status (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
jcid VARCHAR(64) NOT NULL,
sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
last_sync_at DATETIME(3) NULL,
error_code VARCHAR(64) NULL,
error_msg VARCHAR(512) NULL,
retry_count INT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_synst_gyid_jcid (gyid, jcid),
KEY idx_synst_gyid_state (gyid, sync_state, last_sync_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='计划同步状态 [推断]';
-- 10 叫乘任务租约锁(防止重复生成任务)
CREATE TABLE IF NOT EXISTS gy_call_duty_match_lock (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
lock_key VARCHAR(128) NOT NULL COMMENT '租约键',
jcid VARCHAR(64) NULL,
owner_ref VARCHAR(64) NULL COMMENT '持锁实例',
lease_until DATETIME(3) NOT NULL COMMENT '租约到期',
state VARCHAR(20) NOT NULL DEFAULT 'HELD',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_lock_gyid_key (gyid, lock_key),
KEY idx_lock_gyid_lease (gyid, lease_until),
CONSTRAINT ck_lock_state CHECK (state IN ('HELD','RELEASED','EXPIRED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='叫乘任务租约锁 [推断]';
-- 11 本地建单 / 入住申请(CR-01 表 1-17 + 公寓端/11 表 11-7 补列)
-- Q-40 未冻结前:扩展要素落 request_payload JSON 兜底,不新增列(表 13-10)
CREATE TABLE IF NOT EXISTS apt_local_task (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
task_id VARCHAR(64) NOT NULL COMMENT '本地任务 ID(幂等键)',
jcid VARCHAR(64) NULL COMMENT '预分配叫乘 ID,与 apply_id 一对一',
apply_id VARCHAR(64) NULL COMMENT '入住申请 ID',
origin VARCHAR(20) NOT NULL DEFAULT 'LOCAL' COMMENT 'LOCAL/PLATFORM/DEVICE',
local_version BIGINT NOT NULL DEFAULT 1,
biz_version BIGINT NOT NULL DEFAULT 1,
match_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/MATCHED/UNMATCHED/LOCKED/REJECTED',
checkin_state VARCHAR(24) NOT NULL DEFAULT 'SUBMITTED' COMMENT 'SUBMITTED/AUDITED/CHECKED_IN/REJECTED',
sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SENT/FAILED/DEAD/RETRIED',
fjh VARCHAR(32) NULL,
fjh_final VARCHAR(32) NULL COMMENT '审核后房间',
room_adjust_seq INT NOT NULL DEFAULT 0,
face_verify_state VARCHAR(20) NULL COMMENT 'VERIFIED/UNVERIFIED/SKIPPED',
face_device_code VARCHAR(64) NULL,
face_verified_at DATETIME(3) NULL,
ryid VARCHAR(64) NULL,
xm VARCHAR(64) NULL,
jlid VARCHAR(64) NULL,
cc VARCHAR(64) NULL,
jcsj DATETIME(3) NULL,
request_payload JSON NULL COMMENT 'Q-40 冻结前的 JSON 兜底留痕',
audited_by VARCHAR(64) NULL,
audited_at DATETIME(3) NULL,
checked_in_at DATETIME(3) NULL,
reject_reason VARCHAR(256) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_lt_gyid_task (gyid, task_id),
UNIQUE KEY uk_lt_gyid_jcid (gyid, jcid),
KEY idx_lt_gyid_apply (gyid, apply_id),
KEY idx_lt_gyid_state (gyid, match_state, checkin_state),
CONSTRAINT ck_lt_match CHECK (match_state IN ('PENDING','MATCHED','UNMATCHED','LOCKED','REJECTED')),
CONSTRAINT ck_lt_checkin CHECK (checkin_state IN ('SUBMITTED','AUDITED','CHECKED_IN','REJECTED')),
CONSTRAINT ck_lt_sync CHECK (sync_state IN ('PENDING','SENT','FAILED','DEAD','RETRIED')),
CONSTRAINT ck_lt_face CHECK (face_verify_state IS NULL OR face_verify_state IN ('VERIFIED','UNVERIFIED','SKIPPED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='本地建单/入住申请(申请阶段不建 gy_jcxx 行)';
@@ -0,0 +1,254 @@
-- =============================================================================
-- 域 2:执行域(7 张)+ 设备域(4 张)= 11 张
-- 来源:公寓端/03 表 3-3 / 3-4;字段口径见 公寓端/03 表 3-9、表 3-11、表 3-15
-- 说明:gy_jcxx 存量 70 列(docs/11 表 2-1),此处按接口契约给出核心列 + CR-02 表 3-32
-- 两处补列;其余存量列在阶段二冻结后由 07_patch_columns.sql 幂等补齐。
-- 硬约束:ryzt / finish_jc / finish_ly / stoped 只有 apt.duty.state 一个写入口,
-- SQL 必须带 gyid 与条件更新(where id=? and ryzt=?),禁止直接 UPDATE。
-- =============================================================================
-- 12 叫乘主表(执行事实 + 业务终态)
CREATE TABLE IF NOT EXISTS gy_jcxx (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
jcid VARCHAR(64) NOT NULL COMMENT '叫乘 ID',
jlid VARCHAR(64) NULL,
cc VARCHAR(64) NULL,
ryid VARCHAR(64) NULL,
xm VARCHAR(64) NULL,
fjh VARCHAR(32) NULL,
kcsj DATETIME(3) NULL COMMENT '开车时刻',
jcsj DATETIME(3) NULL COMMENT '叫乘时刻(公寓复算,权威)',
jcsj_platform DATETIME(3) NULL COMMENT '平台计划基线(仅对照)',
ryzt VARCHAR(8) NOT NULL DEFAULT '0' COMMENT '叫乘状态 0~5(存量数字码,不跨端)',
finish_jc VARCHAR(2) NOT NULL DEFAULT '0' COMMENT '0 未完成 / 1 已完成',
finish_ly VARCHAR(2) NOT NULL DEFAULT '0' COMMENT '0 在寓 / 1 已离寓',
stoped VARCHAR(2) NOT NULL DEFAULT '0' COMMENT '人工停止',
sjlysj DATETIME(3) NULL COMMENT '实际离寓时间;与 finish_ly 共同判「已入住」',
call_mode VARCHAR(20) NOT NULL DEFAULT 'SIP' COMMENT 'CR-02 表 3-32 补列:SIP / TABLET',
dispatch_seq BIGINT NULL COMMENT 'CR-02 表 3-32 补列:平板派发序号',
biz_version BIGINT NOT NULL DEFAULT 1 COMMENT '条件更新乐观锁',
occurred_at DATETIME(3) NULL COMMENT '业务发生时刻(不判日切)',
received_at DATETIME(3) NULL COMMENT '本地入库时刻',
platform_recv_at DATETIME(3) NULL COMMENT '平台接收时刻(归日以此为准)',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_jcxx_gyid_jcid (gyid, jcid),
KEY idx_jcxx_gyid_time (gyid, jcsj),
KEY idx_jcxx_gyid_state (gyid, ryzt, finish_ly),
KEY idx_jcxx_gyid_seq (gyid, dispatch_seq),
CONSTRAINT ck_jcxx_ryzt CHECK (ryzt IN ('0','1','2','3','4','5')),
CONSTRAINT ck_jcxx_finish_jc CHECK (finish_jc IN ('0','1')),
CONSTRAINT ck_jcxx_finish_ly CHECK (finish_ly IN ('0','1')),
CONSTRAINT ck_jcxx_stoped CHECK (stoped IN ('0','1')),
CONSTRAINT ck_jcxx_mode CHECK (call_mode IN ('SIP','TABLET'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='叫乘主表;状态字段单写入口 apt.duty.state';
-- 13 叫乘归档(只读,#13 ARCHIVE 摘要来源)
CREATE TABLE IF NOT EXISTS gy_jcxx_his (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
jcid VARCHAR(64) NOT NULL,
jlid VARCHAR(64) NULL,
cc VARCHAR(64) NULL,
ryid VARCHAR(64) NULL,
xm VARCHAR(64) NULL,
fjh VARCHAR(32) NULL,
jcsj DATETIME(3) NULL,
ryzt VARCHAR(8) NULL,
finish_jc VARCHAR(2) NULL,
finish_ly VARCHAR(2) NULL,
sjlysj DATETIME(3) NULL,
archive_batch VARCHAR(64) NULL,
archived_at DATETIME(3) NULL,
summary_payload JSON NULL COMMENT '§15 分组计数 satisfaction{} / roomOccupancy{}',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_jcxxhis_gyid_jcid (gyid, jcid),
KEY idx_jcxxhis_gyid_date (gyid, archived_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='叫乘归档历史 [推断]';
-- 14 离寓/入住事件(设备与人工登记事实)
CREATE TABLE IF NOT EXISTS gygl_ly_event (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
event_id VARCHAR(64) NOT NULL COMMENT '事件幂等键',
event_type VARCHAR(20) NOT NULL COMMENT 'CHECKIN / CHECKOUT',
event_source VARCHAR(20) NOT NULL COMMENT 'DEVICE / FACE / MANUAL',
jcid VARCHAR(64) NULL,
ryid VARCHAR(64) NULL,
fjh VARCHAR(32) NULL,
occurred_at DATETIME(3) NOT NULL,
received_at DATETIME(3) NULL,
platform_recv_at DATETIME(3) NULL,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SENT/FAILED/DEAD/RETRIED',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_lye_gyid_event (gyid, event_id),
KEY idx_lye_gyid_type_time (gyid, event_type, occurred_at),
CONSTRAINT ck_lye_type CHECK (event_type IN ('CHECKIN','CHECKOUT')),
CONSTRAINT ck_lye_source CHECK (event_source IN ('DEVICE','FACE','MANUAL')),
CONSTRAINT ck_lye_report CHECK (report_state IN ('PENDING','SENT','FAILED','DEAD','RETRIED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='离寓/入住事件 [推断+§13]';
-- 15 房态(唯一写入口 apt-stay,条件更新带 state_version)
CREATE TABLE IF NOT EXISTS gy_rooms (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
room_no VARCHAR(32) NOT NULL,
room_floor VARCHAR(32) NULL,
room_status VARCHAR(8) NOT NULL DEFAULT '1' COMMENT '1 空闲/2 住满/3 待清扫/4 不能入住/5 未满/6 保洁中',
in_person_count INT NOT NULL DEFAULT 0,
state_version BIGINT NOT NULL DEFAULT 1 COMMENT '乐观锁:where id=? and state_version=?',
disabled_reason VARCHAR(256) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_rooms_gyid_room (gyid, room_id),
KEY idx_rooms_gyid_status (gyid, room_status, room_floor),
CONSTRAINT ck_rooms_status CHECK (room_status IN ('1','2','3','4','5','6'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='房间与房态(apt-stay 写入)';
-- 16 保洁任务
CREATE TABLE IF NOT EXISTS gy_clean_task (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
task_id VARCHAR(64) NOT NULL,
room_id VARCHAR(64) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/PROCESSING/DONE/CANCELLED',
assignee_ref VARCHAR(64) NULL,
scheduled_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_cleantask_gyid_task (gyid, task_id),
KEY idx_cleantask_gyid_state (gyid, state, scheduled_at),
CONSTRAINT ck_cleantask_state CHECK (state IN ('PENDING','PROCESSING','DONE','CANCELLED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='保洁任务 [推断]';
-- 17 报警记录
CREATE TABLE IF NOT EXISTS gy_bjxx (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
alarm_id VARCHAR(64) NOT NULL,
jcid VARCHAR(64) NULL,
room_id VARCHAR(64) NULL,
alarm_type VARCHAR(32) NOT NULL,
severity VARCHAR(20) NOT NULL DEFAULT 'WARN',
occurred_at DATETIME(3) NOT NULL,
handled_at DATETIME(3) NULL,
handled_by VARCHAR(64) NULL,
handled_result VARCHAR(256) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_bjxx_gyid_alarm (gyid, alarm_id),
KEY idx_bjxx_gyid_type_time (gyid, alarm_type, occurred_at),
KEY idx_bjxx_gyid_handled (gyid, handled_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='报警记录 [推断]';
-- 18 报警日汇总(#13 摘要来源)
CREATE TABLE IF NOT EXISTS gy_bjcstj (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
stat_date DATE NOT NULL,
alarm_count INT NOT NULL DEFAULT 0,
handled_count INT NOT NULL DEFAULT 0,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_bjcstj_gyid_date (gyid, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='报警日汇总 [推断]';
-- 19 设备档案(含 §12 人脸设备范围补列)
CREATE TABLE IF NOT EXISTS gy_sb (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
device_code VARCHAR(64) NOT NULL COMMENT '设备编码,入库大写',
device_type VARCHAR(32) NOT NULL COMMENT 'TABLET / FACE / SIP / BROADCAST',
room_id VARCHAR(64) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
face_scope_type VARCHAR(20) NULL COMMENT '§12:设备人脸范围类型',
face_scope_ref VARCHAR(128) NULL COMMENT '§12:设备人脸范围引用',
last_seen_at DATETIME(3) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_sb_gyid_code (gyid, device_code),
KEY idx_sb_gyid_type_state (gyid, device_type, state),
CONSTRAINT ck_sb_state CHECK (state IN ('ACTIVE','DISABLED','OFFLINE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备档案(含 §12 补列)';
-- 20 设备心跳与在线状态
CREATE TABLE IF NOT EXISTS gy_sb_xt (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
device_code VARCHAR(64) NOT NULL,
online_state VARCHAR(20) NOT NULL DEFAULT 'OFFLINE',
app_version VARCHAR(32) NULL,
ip_ref VARCHAR(64) NULL COMMENT '只存引用,不落内网地址',
heartbeat_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_sbxt_gyid_code (gyid, device_code),
KEY idx_sbxt_gyid_state (gyid, online_state, heartbeat_at),
CONSTRAINT ck_sbxt_state CHECK (online_state IN ('ONLINE','OFFLINE','UNKNOWN'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备心跳 [推断]';
-- 21 设备历史指令(存量兼容,只读,不新增写入)
CREATE TABLE IF NOT EXISTS gy_sb_zl (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
command_id VARCHAR(64) NOT NULL,
device_code VARCHAR(64) NULL,
command_type VARCHAR(32) NULL,
state VARCHAR(20) NULL,
occurred_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_sbzl_gyid_cmd (gyid, command_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备历史指令(只读存量兼容)[推断]';
-- 22 设备指令队列(只入队 PENDING,无心跳不伪造 ACKED)
CREATE TABLE IF NOT EXISTS apt_device_command (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
command_id VARCHAR(64) NOT NULL COMMENT '命令幂等键',
device_code VARCHAR(64) NOT NULL,
command_type VARCHAR(32) NOT NULL COMMENT 'FACE_ISSUE/FACE_ERASE/FACE_FULL_SYNC/PLAY_TASK',
payload_json JSON NULL,
state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SENT/ACKED/EXPIRED/FAILED',
protocol_confirmed TINYINT NOT NULL DEFAULT 0 COMMENT '默认 0,设备 ACK 前不置 1',
face_scope_ref VARCHAR(128) NULL COMMENT '§12 补列',
sent_at DATETIME(3) NULL,
acked_at DATETIME(3) NULL,
expire_at DATETIME(3) NULL,
last_error VARCHAR(512) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_devcmd_gyid_cmd (gyid, command_id),
KEY idx_devcmd_gyid_state (gyid, state, create_time),
CONSTRAINT ck_devcmd_state CHECK (state IN ('PENDING','SENT','ACKED','EXPIRED','FAILED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备指令队列(§12 补列)';
+227
View File
@@ -0,0 +1,227 @@
-- =============================================================================
-- 域 3:语音域(11 张)+ §10 补遗 T-17 apt_sip_reg_status(1 张)= 12 张
-- 来源:公寓端/03 表 3-5;docs/10 表 5-3(PK/UK/IDX/关键字段)、表 5-4(MySQL 类型适配);
-- 公寓端/00 表 0-6~0-8(endpoint / session / recording 逐列)、表 0-9(T-17 逐列);
-- CR-02 表 3-40(T-17 关键约束:只追加、无 update_time)
-- 约束:分机号不可原地改号(改号返回 40906);sync_state 仅 4 值 + CHECK;
-- 录音只存引用与校验和,不落二进制;表上不建外键。
-- =============================================================================
-- 23 分机台账
CREATE TABLE IF NOT EXISTS apt_sip_endpoint (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
endpoint_id VARCHAR(64) NOT NULL,
extension VARCHAR(8) NOT NULL COMMENT '分机号 char(8),入库统一大写',
room_id VARCHAR(64) NULL,
display_name VARCHAR(64) NULL,
config_version BIGINT NOT NULL DEFAULT 1 COMMENT '分机配置版本(区别于 state_version/biz_version)',
sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '仅 PENDING/SYNCED/FAILED/DRIFT',
enabled TINYINT NOT NULL DEFAULT 1,
last_qualify_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_endpoint_gyid_id (gyid, endpoint_id),
UNIQUE KEY uk_endpoint_gyid_ext (gyid, extension),
KEY idx_endpoint_gyid_sync (gyid, sync_state),
CONSTRAINT ck_endpoint_sync CHECK (sync_state IN ('PENDING','SYNCED','FAILED','DRIFT'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='SIP 分机台账(先库后 Asterisk + 异步收敛)';
-- 24 分机-房间绑定
CREATE TABLE IF NOT EXISTS apt_sip_binding (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
binding_id VARCHAR(64) NOT NULL,
endpoint_id VARCHAR(64) NOT NULL,
room_id VARCHAR(64) NOT NULL,
effective_from DATETIME(3) NOT NULL,
effective_to DATETIME(3) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_binding_gyid_id (gyid, binding_id),
KEY idx_binding_gyid_room (gyid, room_id, state)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='分机绑定 [推断]';
-- 25 中继
CREATE TABLE IF NOT EXISTS apt_trunk (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
trunk_id VARCHAR(64) NOT NULL,
trunk_name VARCHAR(64) NULL,
host_ref VARCHAR(128) NULL COMMENT '只存引用,不落内网地址',
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
config_version BIGINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_trunk_gyid_id (gyid, trunk_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='中继 [推断]';
-- 26 通话会话
CREATE TABLE IF NOT EXISTS apt_call_session (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
session_id VARCHAR(64) NOT NULL,
jcid VARCHAR(64) NULL,
endpoint_id VARCHAR(64) NULL,
conf_no VARCHAR(64) NULL COMMENT '单 Conf-01 兼容值,不加唯一约束',
call_mode VARCHAR(20) NOT NULL DEFAULT 'SIP',
call_state VARCHAR(20) NOT NULL DEFAULT 'INIT',
started_at DATETIME(3) NULL,
answered_at DATETIME(3) NULL,
ended_at DATETIME(3) NULL,
hangup_cause VARCHAR(64) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_callsession_gyid_id (gyid, session_id),
KEY idx_callsession_gyid_jcid (gyid, jcid),
KEY idx_callsession_gyid_state (gyid, call_state, started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='通话会话(ConfbridgeJoin 判接通)';
-- 27 AMI 事件(只追加,明细不上报)
CREATE TABLE IF NOT EXISTS apt_call_event (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
event_seq BIGINT NOT NULL COMMENT '同会话单调',
event_id VARCHAR(64) NOT NULL,
session_id VARCHAR(64) NULL,
event_type VARCHAR(32) NOT NULL,
event_payload JSON NULL,
occurred_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_callevent_gyid_id (gyid, event_id),
UNIQUE KEY uk_callevent_gyid_seq (gyid, session_id, event_seq),
KEY idx_callevent_gyid_time (gyid, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AMI 事件(只追加)[推断]';
-- 28 AMI 事件归档(只读)
CREATE TABLE IF NOT EXISTS apt_call_event_his (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
event_id VARCHAR(64) NOT NULL,
event_seq BIGINT NOT NULL,
session_id VARCHAR(64) NULL,
event_type VARCHAR(32) NULL,
event_payload JSON NULL,
occurred_at DATETIME(3) NULL,
archived_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_calleventhis_gyid_id (gyid, event_id),
KEY idx_calleventhis_gyid_date (gyid, archived_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AMI 事件归档 [推断]';
-- 29 录音索引(只存引用,PURGED 返回 41001)
CREATE TABLE IF NOT EXISTS apt_recording (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
recording_id VARCHAR(64) NOT NULL,
session_id VARCHAR(64) NULL,
file_ref VARCHAR(512) NULL COMMENT '签名地址引用,不落二进制',
checksum VARCHAR(128) NULL,
duration_ms BIGINT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'RECORDING' COMMENT 'RECORDING/AVAILABLE/MISSING/PURGED',
expire_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_recording_gyid_id (gyid, recording_id),
KEY idx_recording_gyid_session (gyid, session_id),
CONSTRAINT ck_recording_state CHECK (state IN ('RECORDING','AVAILABLE','MISSING','PURGED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='录音索引(§12:录音不出公寓)';
-- 30 TTS 播报记录
CREATE TABLE IF NOT EXISTS apt_tts_utterance (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
utterance_id VARCHAR(64) NOT NULL,
text_ref VARCHAR(256) NULL,
audio_ref VARCHAR(512) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tts_gyid_id (gyid, utterance_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='TTS 播报 [推断]';
-- 31 Asterisk 配置版本
CREATE TABLE IF NOT EXISTS apt_ast_config_version (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
version_id VARCHAR(64) NOT NULL,
config_type VARCHAR(32) NOT NULL,
config_version BIGINT NOT NULL DEFAULT 1,
checksum VARCHAR(128) NULL,
applied_at DATETIME(3) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_astcfg_gyid_ver (gyid, version_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Asterisk 配置版本 [推断]';
-- 32 配置漂移对账
CREATE TABLE IF NOT EXISTS apt_ast_drift (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
drift_id VARCHAR(64) NOT NULL,
endpoint_id VARCHAR(64) NULL,
drift_type VARCHAR(32) NOT NULL,
expected_ref VARCHAR(256) NULL,
actual_ref VARCHAR(256) NULL,
detected_at DATETIME(3) NOT NULL,
resolved_at DATETIME(3) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'OPEN',
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_astdrift_gyid_id (gyid, drift_id),
KEY idx_astdrift_gyid_state (gyid, state, detected_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='配置漂移 [推断]';
-- 33 AMI 会话
CREATE TABLE IF NOT EXISTS apt_ast_ami_session (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
ami_session_id VARCHAR(64) NOT NULL,
connected_at DATETIME(3) NULL,
disconnected_at DATETIME(3) NULL,
reconnect_count INT NOT NULL DEFAULT 0,
state VARCHAR(20) NOT NULL DEFAULT 'DOWN',
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_amisession_gyid_id (gyid, ami_session_id),
KEY idx_amisession_gyid_state (gyid, state, connected_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AMI 会话 [推断]';
-- 34 T-17 分机注册状态流水(CR-02 §10 补遗 / 公寓端/00 表 0-9)
-- 只追加、无 update_time;event_seq 同分机单调;恢复只追加不清空
CREATE TABLE IF NOT EXISTS apt_sip_reg_status (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '代理主键',
gyid VARCHAR(32) NOT NULL,
endpoint_id VARCHAR(64) NOT NULL,
event_seq BIGINT NOT NULL COMMENT '同分机单调递增',
state VARCHAR(20) NOT NULL COMMENT 'REGISTERED/UNREGISTERED/UNREACHABLE/UNKNOWN',
prev_state VARCHAR(20) NULL,
source VARCHAR(20) NOT NULL COMMENT 'AMI_EVENT/QUALIFY/PROBE/MANUAL/RECONCILE',
alarm_id VARCHAR(64) NULL COMMENT '关联 apt_device_alarm',
alarm_event_key VARCHAR(128) NULL COMMENT 'hash(gyid,endpoint_id,SIP_OFFLINE,offline_since)',
offline_since DATETIME(3) NULL,
occurred_at DATETIME(3) NOT NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_sipreg_gyid_seq (gyid, endpoint_id, event_seq),
KEY idx_sipreg_gyid_state_time (gyid, state, occurred_at),
KEY idx_sipreg_gyid_ep_time (gyid, endpoint_id, occurred_at),
CONSTRAINT ck_sipreg_state CHECK (state IN ('REGISTERED','UNREGISTERED','UNREACHABLE','UNKNOWN')),
CONSTRAINT ck_sipreg_source CHECK (source IN ('AMI_EVENT','QUALIFY','PROBE','MANUAL','RECONCILE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-17 分机注册状态流水(只追加)';
+145
View File
@@ -0,0 +1,145 @@
-- =============================================================================
-- 域 4:账务本地域(5 张)+ T-11 apt_ledger_offline(1 张)= 6 张
-- 来源:公寓端/03 表 3-6;公寓端/12 表 12-7~12-9(双额度 / 状态链 / 补列);
-- CR-02 表 3-30(T-11 列级定义);CR-C34 金额口径、CR-C37 现金充值计收入
-- 重要:本域属「延后域」(IT-9~IT-11 / P2)。按 公寓端/13 表 13-29,
-- 第一轮**只建表结构、预置错误码与权限点,不开放任何读写、不建业务代码**;
-- 平台统计账务三类指标按 NO_DATA / BLOCKED 输出,不推断不补零。
-- =============================================================================
-- 35 余额主数据(公寓本地缓存;platform-ledger 为余额主账唯一写者)
CREATE TABLE IF NOT EXISTS gy_ykt_remainder (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
ryid VARCHAR(64) NOT NULL,
xm VARCHAR(64) NULL,
knye DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '卡内/补贴额度',
xjye DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '现金额度(充值只增此列)',
xfze DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '消费总额',
bzze DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '补助总额',
xfxjje DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '现金消费额',
xfbzje DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '补助消费额',
balance_version BIGINT NOT NULL DEFAULT 1 COMMENT '平台主账版本',
local_balance_version BIGINT NOT NULL DEFAULT 1 COMMENT '本地 CAS 版本',
last_reconciled_on DATE NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_remainder_gyid_ryid (gyid, ryid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='双额度余额缓存(延后域,本期不读写)';
-- 36 消费流水(一笔消费只一条 flow_id)
CREATE TABLE IF NOT EXISTS gy_ykt_consume (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
flow_id VARCHAR(64) NOT NULL COMMENT '消费流水幂等键',
ryid VARCHAR(64) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
allowance_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '补贴部分(CR-C38 统计取此列)',
cash_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '现金补足部分',
recipe_version VARCHAR(64) NULL,
device_code VARCHAR(64) NULL,
device_event_id VARCHAR(64) NULL COMMENT '设备事件幂等键',
ledger_state VARCHAR(24) NOT NULL DEFAULT 'SUBMITTED' COMMENT 'SUBMITTED/DEDUCTED/REPORTED/RECONCILED/REJECTED/REVERSED',
offline_scope VARCHAR(20) NOT NULL DEFAULT 'LOCAL_ONLY' COMMENT 'LOCAL_ONLY/SYNCED/RECONCILED',
occurred_at DATETIME(3) NOT NULL,
received_at DATETIME(3) NULL,
platform_recv_at DATETIME(3) NULL COMMENT '归日以此为准',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_consume_gyid_flow (gyid, flow_id),
UNIQUE KEY uk_consume_gyid_devevt (gyid, device_event_id),
KEY idx_consume_gyid_time (gyid, occurred_at),
CONSTRAINT ck_consume_ledger CHECK (ledger_state IN ('SUBMITTED','DEDUCTED','REPORTED','RECONCILED','REJECTED','REVERSED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='消费流水(延后域,本期不读写)';
-- 37 补贴发放
CREATE TABLE IF NOT EXISTS gy_ykt_allowance (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
allowance_id VARCHAR(64) NOT NULL,
ryid VARCHAR(64) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
period VARCHAR(20) NULL,
issue_at DATETIME(3) NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ISSUED',
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_allowance_gyid_id (gyid, allowance_id),
KEY idx_allowance_gyid_ryid (gyid, ryid, issue_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='补贴发放(延后域,本期不读写)[推断]';
-- 38 线下充值申请(申请表不存金额,金额由管理员核对后写入记录)
CREATE TABLE IF NOT EXISTS gy_ykt_cash_request (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
request_id VARCHAR(64) NOT NULL,
ryid VARCHAR(64) NOT NULL,
request_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/COMPLETED/CANCELLED',
confirmed_by VARCHAR(64) NULL,
confirmed_at DATETIME(3) NULL,
cancel_reason VARCHAR(256) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_cashreq_gyid_id (gyid, request_id),
KEY idx_cashreq_gyid_state (gyid, request_state, create_time),
CONSTRAINT ck_cashreq_state CHECK (request_state IN ('PENDING','COMPLETED','CANCELLED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='线下充值申请(申请表不存金额)';
-- 39 现金充值记录(计收入,不与消费抵销 CR-C37)
CREATE TABLE IF NOT EXISTS gy_ykt_xjczjl (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
flow_id VARCHAR(64) NOT NULL,
request_id VARCHAR(64) NULL,
ryid VARCHAR(64) NOT NULL,
amount DECIMAL(12,2) NOT NULL COMMENT '正数,计期间收入',
recharge_channel VARCHAR(20) NOT NULL DEFAULT 'OFFLINE_CASH',
occurred_at DATETIME(3) NOT NULL,
platform_recv_at DATETIME(3) NULL,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_xjczjl_gyid_flow (gyid, flow_id),
KEY idx_xjczjl_gyid_time (gyid, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='现金充值记录(延后域,本期不读写)';
-- 40 T-11 断网本地流水(CR-02 表 3-30 列级定义 + §14 补列)
CREATE TABLE IF NOT EXISTS apt_ledger_offline (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
flow_id VARCHAR(64) NOT NULL,
type VARCHAR(30) NOT NULL COMMENT 'CONSUME / RECHARGE',
ryid VARCHAR(64) NOT NULL,
card_no VARCHAR(64) NULL,
device_code VARCHAR(64) NULL,
amount DECIMAL(12,2) NOT NULL COMMENT '扣减记负数,充值记正数',
allowance_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '§14 补列:补贴部分',
cash_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '§14 补列:现金部分',
balance_before DECIMAL(12,2) NOT NULL COMMENT 'CAS 前值',
balance_after DECIMAL(12,2) NOT NULL COMMENT 'CAS 后值',
offline_scope VARCHAR(20) NOT NULL DEFAULT 'LOCAL_ONLY' COMMENT 'LOCAL_ONLY/SYNCED/RECONCILED',
local_balance_version BIGINT NOT NULL DEFAULT 1,
quota_ref VARCHAR(64) NULL COMMENT '#1 离线额度批次',
idempotency_key VARCHAR(128) NULL COMMENT 'hash(gyId+bizType+bizId+bizVersion)',
occurred_at DATETIME(3) NOT NULL,
synced_at DATETIME(3) NULL,
platform_recv_at DATETIME(3) NULL,
reconcile_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RECONCILED/DIFF/REVERSED',
last_error VARCHAR(512) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_ledgeroff_gyid_type_flow (gyid, type, flow_id),
KEY idx_ledgeroff_gyid_state (gyid, reconcile_state, occurred_at),
CONSTRAINT ck_ledgeroff_type CHECK (type IN ('CONSUME','RECHARGE')),
CONSTRAINT ck_ledgeroff_scope CHECK (offline_scope IN ('LOCAL_ONLY','SYNCED','RECONCILED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-11 断网本地账务流水(延后域)';
@@ -0,0 +1,215 @@
-- =============================================================================
-- 域 5:上报 / 值班 / 运维(7 张)+ T-09 / T-10 / T-12 / T-13(4 张)= 11 张
-- 来源:公寓端/03 表 3-7;CR-02 表 3-29(T-12/T-13)、表 3-30(T-09/T-10)、表 3-32(补列)
-- 关键区分:apt_report_outbox 是「队列」(本地状态机 PENDING/SENDING/SENT/FAILED/DEAD,
-- 见 公寓端/03 表 3-11),apt_report_history 是「终态流水」,两者不互换(CR-C10)。
-- 跨端信封 reportState 正式五值 PENDING/SENT/FAILED/DEAD/RETRIED(CR-02 表 3-21)。
-- =============================================================================
-- 41 上报 outbox 队列(含 CR-02 表 3-32 补列 retry_batch_id)
CREATE TABLE IF NOT EXISTS apt_report_outbox (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL COMMENT 'hash(gyId+bizType+bizId+bizVersion)',
biz_type VARCHAR(64) NOT NULL COMMENT '既有 bizType,不新增枚举',
biz_id VARCHAR(64) NOT NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
payload JSON NULL,
state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '本地队列态(表 3-11)',
retry_batch_id VARCHAR(64) NULL COMMENT 'CR-02 表 3-32 补列',
retry_count INT NOT NULL DEFAULT 0,
watermark BIGINT NOT NULL DEFAULT 0 COMMENT '对账三维 max(watermark)',
next_retry_at DATETIME(3) NULL,
last_error VARCHAR(512) NULL,
occurred_at DATETIME(3) NOT NULL,
received_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_outbox_gyid_idem (gyid, idempotency_key),
KEY idx_outbox_gyid_state_wm (gyid, state, watermark),
KEY idx_outbox_gyid_biz (gyid, biz_type, biz_id),
CONSTRAINT ck_outbox_state CHECK (state IN ('PENDING','SENDING','SENT','FAILED','DEAD'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='上报 outbox 队列(非终态流水)';
-- 42 上报水位
CREATE TABLE IF NOT EXISTS apt_report_watermark (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
current_watermark BIGINT NOT NULL DEFAULT 0,
acked_watermark BIGINT NOT NULL DEFAULT 0,
last_heartbeat_at DATETIME(3) NULL,
clock_skew_ms BIGINT NULL COMMENT '#14 心跳时钟偏差',
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_watermark_gyid (gyid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='上报水位 [推断]';
-- 43 值班操作审计(所有写操作经审计切面;Q-40 冻结前也作 JSON 兜底载体)
CREATE TABLE IF NOT EXISTS duty_operation_log (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
log_id VARCHAR(64) NOT NULL,
operator_ref VARCHAR(64) NULL,
module VARCHAR(64) NULL,
action VARCHAR(64) NULL,
detail JSON NULL COMMENT 'Q-40 / Q-48 冻结前的 JSON 兜底留痕',
trace_id VARCHAR(64) NULL,
occurred_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_oplog_gyid_log (gyid, log_id),
KEY idx_oplog_gyid_time (gyid, occurred_at),
KEY idx_oplog_gyid_trace (gyid, trace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='值班操作审计 [推断]';
-- 44 人工命令(只提交命令,由 apt.duty.state 落状态)
CREATE TABLE IF NOT EXISTS duty_manual_action (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
action_id VARCHAR(64) NOT NULL COMMENT '与 command_id 对齐,幂等键',
jcid VARCHAR(64) NULL,
action_type VARCHAR(32) NOT NULL COMMENT 'STOP / RE_CALL / CORRECT',
state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
duplicated TINYINT NOT NULL DEFAULT 0 COMMENT '重复命令返回 duplicated=true',
result_code VARCHAR(64) NULL,
requested_by VARCHAR(64) NULL,
occurred_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_manual_gyid_action (gyid, action_id),
KEY idx_manual_gyid_jcid (gyid, jcid, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='人工命令(不直写业务状态)[推断]';
-- 45 交班日志
CREATE TABLE IF NOT EXISTS duty_shift_handover (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
handover_id VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NULL,
from_operator VARCHAR(64) NULL,
to_operator VARCHAR(64) NULL,
content JSON NULL,
handed_at DATETIME(3) NULL,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_handover_gyid_id (gyid, handover_id),
KEY idx_handover_gyid_time (gyid, handed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='交班日志 [推断]';
-- 46 运维自检(12 项)
CREATE TABLE IF NOT EXISTS apt_ops_selfcheck (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
check_id VARCHAR(64) NOT NULL,
check_item VARCHAR(64) NOT NULL,
result VARCHAR(20) NOT NULL DEFAULT 'UNKNOWN',
call_ready TINYINT NOT NULL DEFAULT 0,
detail VARCHAR(512) NULL,
checked_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_selfcheck_gyid_id (gyid, check_id),
KEY idx_selfcheck_gyid_time (gyid, checked_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='运维自检 12 项 [推断]';
-- 47 人员账号映射
CREATE TABLE IF NOT EXISTS apt_person_account (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
ryid VARCHAR(64) NOT NULL,
sys_user_id BIGINT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
source_version BIGINT NOT NULL DEFAULT 0,
deleted TINYINT NOT NULL DEFAULT 0,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_personacct_gyid_ryid (gyid, ryid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='人员账号映射 [推断]';
-- 48 T-09 上报终态流水(CR-02 表 3-30;不是队列)
CREATE TABLE IF NOT EXISTS apt_report_history (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
biz_type VARCHAR(64) NOT NULL,
biz_id VARCHAR(64) NOT NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
batch_id VARCHAR(64) NULL,
retry_batch_id VARCHAR(64) NULL,
state VARCHAR(20) NOT NULL COMMENT 'SENT/FAILED/DEAD/RETRIED',
result_code VARCHAR(64) NULL,
last_error VARCHAR(512) NULL,
occurred_at DATETIME(3) NULL,
sent_at DATETIME(3) NULL,
platform_recv_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_reporthis_gyid_idem (gyid, idempotency_key),
KEY idx_reporthis_gyid_state (gyid, state, sent_at),
CONSTRAINT ck_reporthis_state CHECK (state IN ('SENT','FAILED','DEAD','RETRIED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-09 上报终态流水';
-- 49 T-10 续报批次(CR-02 表 3-30)
CREATE TABLE IF NOT EXISTS apt_report_retry_batch (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
batch_id VARCHAR(64) NOT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'RUNNING',
trigger_type VARCHAR(20) NOT NULL DEFAULT 'MANUAL' COMMENT 'MANUAL / AUTO',
item_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
from_watermark BIGINT NOT NULL DEFAULT 0,
to_watermark BIGINT NOT NULL DEFAULT 0,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
created_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_retrybatch_gyid_id (gyid, batch_id),
CONSTRAINT ck_retrybatch_trigger CHECK (trigger_type IN ('MANUAL','AUTO'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-10 续报批次(复用原幂等键)';
-- 50 T-12 排班(CR-02 表 3-29)
CREATE TABLE IF NOT EXISTS duty_roster (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
shift_id VARCHAR(64) NOT NULL COMMENT '日期 + 班次',
ryid VARCHAR(64) NOT NULL,
shift_date DATE NOT NULL,
shift_type VARCHAR(20) NOT NULL DEFAULT 'DAY' COMMENT 'DAY / NIGHT',
start_at DATETIME(3) NOT NULL,
end_at DATETIME(3) NOT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'PLANNED' COMMENT 'PLANNED/CONFIRMED/CANCELLED',
created_by VARCHAR(64) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_roster_gyid_shift_ryid (gyid, shift_id, ryid),
KEY idx_roster_gyid_date (gyid, shift_date),
CONSTRAINT ck_roster_state CHECK (state IN ('PLANNED','CONFIRMED','CANCELLED')),
CONSTRAINT ck_roster_type CHECK (shift_type IN ('DAY','NIGHT'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-12 排班';
-- 51 T-13 盯控质量考核(CR-02 表 3-29;本地算、平台汇总)
CREATE TABLE IF NOT EXISTS duty_performance (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
ryid VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NULL,
stat_date DATE NOT NULL,
response_sec_avg DECIMAL(10,2) NOT NULL DEFAULT 0.00,
alarm_handled_rate DECIMAL(10,2) NOT NULL DEFAULT 0.00,
call_success_rate DECIMAL(10,2) NOT NULL DEFAULT 0.00,
manual_action_count INT NOT NULL DEFAULT 0,
alarm_missed_count INT NOT NULL DEFAULT 0,
calc_version BIGINT NOT NULL DEFAULT 1,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
calculated_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_perf_gyid_date_ryid (gyid, stat_date, ryid),
KEY idx_perf_gyid_date (gyid, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-13 盯控质量考核';
@@ -0,0 +1,196 @@
-- =============================================================================
-- 域 6:CR-02 新增业务表 T-01~T-08(8 张)
-- 列级定义来源:CR-02 表 3-27(T-01~T-03)、表 3-28(T-04/T-05/T-08)、
-- 表 3-29(T-06/T-07);§12 表 10-6 的补列已合并进 T-04
-- 迁移顺序:按 CR-02 表 3-33,本域在存量 44 张之后执行;DDL 与回填分离。
-- =============================================================================
-- 52 T-01 平板设备
CREATE TABLE IF NOT EXISTS apt_tablet_device (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
device_code VARCHAR(64) NOT NULL COMMENT '设备编码,入库大写',
room_id VARCHAR(64) NULL,
activation_code_ref VARCHAR(128) NULL COMMENT '激活码只存引用,不存明文',
register_status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'ACTIVE/DISABLED/PENDING',
app_version VARCHAR(32) NULL,
last_seen_at DATETIME(3) NULL,
last_cursor VARCHAR(128) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tabletdev_gyid_code (gyid, device_code),
CONSTRAINT ck_tabletdev_status CHECK (register_status IN ('ACTIVE','DISABLED','PENDING'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-01 平板设备';
-- 53 T-02 平板任务(按 jcsj 顺序派发,dispatch_seq 单调递增)
CREATE TABLE IF NOT EXISTS apt_tablet_task (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
task_id VARCHAR(64) NOT NULL,
jcid VARCHAR(64) NOT NULL,
dispatch_seq BIGINT NOT NULL,
call_mode VARCHAR(20) NOT NULL DEFAULT 'TABLET',
jcsj DATETIME(3) NULL COMMENT '叫乘基准时刻(本地复算值)',
device_code VARCHAR(64) NULL,
pull_state VARCHAR(20) NOT NULL DEFAULT 'WAITING' COMMENT 'WAITING/PULLED/EXPIRED/DONE',
pulled_at DATETIME(3) NULL,
pulled_by VARCHAR(64) NULL,
expire_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tablettask_gyid_task (gyid, task_id),
KEY idx_tablettask_gyid_seq (gyid, dispatch_seq),
KEY idx_tablettask_gyid_time (gyid, jcsj),
CONSTRAINT ck_tablettask_pull CHECK (pull_state IN ('WAITING','PULLED','EXPIRED','DONE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-02 平板派发任务';
-- 54 T-03 平板叫乘结果(result_id 幂等;平板链路不发 #24)
CREATE TABLE IF NOT EXISTS apt_tablet_call_result (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
result_id VARCHAR(64) NOT NULL COMMENT '幂等键',
task_id VARCHAR(64) NOT NULL,
jcid VARCHAR(64) NOT NULL,
face_verify_state VARCHAR(20) NOT NULL COMMENT 'VERIFIED/UNVERIFIED/SKIPPED',
verified_ryid VARCHAR(64) NULL,
result_type VARCHAR(20) NOT NULL COMMENT 'ANSWERED/NO_ANSWER/FAILED/CANCELLED',
person_count INT NOT NULL DEFAULT 0,
device_code VARCHAR(64) NOT NULL,
occurred_at DATETIME(3) NOT NULL,
received_at DATETIME(3) NOT NULL,
applied_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/APPLIED/IGNORED',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_tabletresult_gyid_id (gyid, result_id),
KEY idx_tabletresult_gyid_task (gyid, task_id),
KEY idx_tabletresult_gyid_state (gyid, applied_state, occurred_at),
CONSTRAINT ck_tabletresult_face CHECK (face_verify_state IN ('VERIFIED','UNVERIFIED','SKIPPED')),
CONSTRAINT ck_tabletresult_type CHECK (result_type IN ('ANSWERED','NO_ANSWER','FAILED','CANCELLED')),
CONSTRAINT ck_tabletresult_applied CHECK (applied_state IN ('PENDING','APPLIED','IGNORED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-03 平板叫乘结果';
-- 55 T-04 人脸注册申请(生效点在公寓;平台回执不决定本地生效)
-- 合并 公寓端/10 表 10-6 补列:双路状态链、媒体引用、person_category
CREATE TABLE IF NOT EXISTS apt_person_face_reg (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
reg_id VARCHAR(64) NOT NULL,
work_no VARCHAR(64) NULL,
name VARCHAR(64) NOT NULL,
face_feature_ref VARCHAR(256) NOT NULL COMMENT '不存特征大对象',
photo_ref VARCHAR(512) NULL COMMENT '不存原图二进制',
photo_checksum VARCHAR(128) NULL,
feature_checksum VARCHAR(128) NULL,
feature_algo VARCHAR(32) NULL,
feature_version BIGINT NOT NULL DEFAULT 1,
person_category VARCHAR(20) NOT NULL DEFAULT 'TRANSIENT' COMMENT 'TRANSIENT / LONG_TERM',
reg_state VARCHAR(20) NOT NULL DEFAULT 'APPLIED' COMMENT 'APPLIED/CONFIRMED/REJECTED/ARCHIVED',
local_only TINYINT(1) NOT NULL DEFAULT 1,
verdict VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '平台同步回执 ACCEPTED/REJECTED',
confirmed_ryid VARCHAR(64) NULL,
reject_reason VARCHAR(256) NULL,
verdict_version BIGINT NULL,
apply_device_code VARCHAR(64) NULL,
applied_at DATETIME(3) NULL,
confirmed_by VARCHAR(64) NULL,
confirmed_at DATETIME(3) NULL,
upload_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '上传平台状态链(独立)',
dispatch_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '下发设备状态链(独立)',
face_sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
registered_by VARCHAR(64) NULL,
device_code VARCHAR(64) NULL,
occurred_at DATETIME(3) NOT NULL,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/SENT/FAILED/DEAD/RETRIED',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_facereg_gyid_reg (gyid, reg_id),
KEY idx_facereg_gyid_state (gyid, reg_state, occurred_at),
KEY idx_facereg_gyid_workno (gyid, work_no),
CONSTRAINT ck_facereg_state CHECK (reg_state IN ('APPLIED','CONFIRMED','REJECTED','ARCHIVED')),
CONSTRAINT ck_facereg_verdict CHECK (verdict IN ('PENDING','ACCEPTED','REJECTED')),
CONSTRAINT ck_facereg_category CHECK (person_category IN ('TRANSIENT','LONG_TERM')),
CONSTRAINT ck_facereg_upload CHECK (upload_state IN ('PENDING','SENT','FAILED','DEAD','ACKED')),
CONSTRAINT ck_facereg_dispatch CHECK (dispatch_state IN ('PENDING','ISSUED','FAILED','DEAD','ACKED')),
CONSTRAINT ck_facereg_report CHECK (report_state IN ('PENDING','SENT','FAILED','DEAD','RETRIED'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-04 人脸注册申请(§12 补列已合并)';
-- 56 T-05 换乘单(CR-02 表 3-28)
CREATE TABLE IF NOT EXISTS apt_transfer_order (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
transfer_id VARCHAR(64) NOT NULL,
jcid VARCHAR(64) NOT NULL,
jlid VARCHAR(64) NOT NULL,
old_room_id VARCHAR(64) NULL,
new_room_id VARCHAR(64) NULL,
new_dep_time DATETIME(3) NOT NULL,
new_call_time DATETIME(3) NOT NULL,
recomputed_call_time DATETIME(3) NOT NULL COMMENT '与 new_call_time 不等时告警,不静默覆盖',
state VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
command_id VARCHAR(64) NULL,
reason VARCHAR(256) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
applied_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_transfer_gyid_id (gyid, transfer_id),
KEY idx_transfer_gyid_jcid (gyid, jcid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-05 换乘单';
-- 57 T-06 保洁人员
CREATE TABLE IF NOT EXISTS apt_clean_staff (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
staff_id VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
phone_ref VARCHAR(128) NULL COMMENT '联系方式只存引用',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_cleanstaff_gyid_id (gyid, staff_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-06 保洁人员';
-- 58 T-07 保洁记录
CREATE TABLE IF NOT EXISTS apt_clean_record (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
record_id VARCHAR(64) NOT NULL,
staff_id VARCHAR(64) NOT NULL,
room_id VARCHAR(64) NOT NULL,
clean_type VARCHAR(20) NOT NULL,
state VARCHAR(20) NOT NULL,
task_id VARCHAR(64) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
remark VARCHAR(256) NULL,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_cleanrecord_gyid_id (gyid, record_id),
KEY idx_cleanrecord_gyid_room (gyid, room_id, started_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-07 保洁记录';
-- 59 T-08 设备报警(CR-02 表 3-28;alarm_event_key 与 /ws/duty 去重一致)
CREATE TABLE IF NOT EXISTS apt_device_alarm (
id BIGINT NOT NULL AUTO_INCREMENT,
gyid VARCHAR(32) NOT NULL,
alarm_id VARCHAR(64) NOT NULL,
device_code VARCHAR(64) NOT NULL,
alarm_source VARCHAR(20) NOT NULL DEFAULT 'DEVICE',
alarm_type VARCHAR(20) NOT NULL,
severity VARCHAR(20) NOT NULL DEFAULT 'WARN',
alarm_event_key VARCHAR(128) NOT NULL,
occurred_at DATETIME(3) NOT NULL,
handled_at DATETIME(3) NULL,
handled_by VARCHAR(64) NULL,
handled_result VARCHAR(256) NULL,
biz_version BIGINT NOT NULL DEFAULT 1,
report_state VARCHAR(20) NOT NULL DEFAULT 'PENDING',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_devalarm_gyid_id (gyid, alarm_id),
UNIQUE KEY uk_devalarm_gyid_key (gyid, alarm_event_key),
KEY idx_devalarm_gyid_state (gyid, alarm_type, occurred_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='T-08 设备报警';
+41
View File
@@ -0,0 +1,41 @@
-- =============================================================================
-- 增量补列(幂等):CR-02 表 3-32 / §12 表 10-6 / §13 表 11-7 / §14 表 12-7~12-9
-- 用法:先执行 00_common.sql 建立 apt_add_column_if_missing,再执行本文件。
-- 说明:本轮 apt_rev01 为空库,01~06 已把补列直接建在表内,本文件为空操作(no-op);
-- 它服务于「已有旧库的后续迭代」与阶段二冻结评审后的增量迁移,重复执行不报错。
-- Q-20 / Q-40 / Q-43 / Q-48 未决项:一律不新增列,按表 13-10 用 JSON 兜底
-- (duty_operation_log.detail / apt_local_task.request_payload / PL_JOB_*)。
-- =============================================================================
CALL apt_add_column_if_missing('gy_jcxx', 'call_mode',
"call_mode VARCHAR(20) NOT NULL DEFAULT 'SIP' COMMENT 'SIP / TABLET'");
CALL apt_add_column_if_missing('gy_jcxx', 'dispatch_seq',
'dispatch_seq BIGINT NULL COMMENT ''平板派发序号''');
CALL apt_add_column_if_missing('apt_report_outbox', 'retry_batch_id',
'retry_batch_id VARCHAR(64) NULL COMMENT ''续报批次''');
CALL apt_add_column_if_missing('apt_master_room', 'building',
'building VARCHAR(64) NULL COMMENT ''楼栋;Q-20 未决前列存在但不启用''');
CALL apt_add_column_if_missing('apt_master_person', 'photo_ref',
'photo_ref VARCHAR(512) NULL COMMENT ''照片引用''');
CALL apt_add_column_if_missing('apt_master_person', 'feature_ref',
'feature_ref VARCHAR(512) NULL COMMENT ''特征值引用''');
CALL apt_add_column_if_missing('apt_master_person', 'face_sync_state',
"face_sync_state VARCHAR(20) NOT NULL DEFAULT 'PENDING'");
CALL apt_add_column_if_missing('apt_local_task', 'apply_id',
'apply_id VARCHAR(64) NULL COMMENT ''入住申请 ID''');
CALL apt_add_column_if_missing('apt_local_task', 'checkin_state',
"checkin_state VARCHAR(24) NOT NULL DEFAULT 'SUBMITTED'");
CALL apt_add_column_if_missing('apt_local_task', 'request_payload',
'request_payload JSON NULL COMMENT ''Q-40 冻结前 JSON 兜底''');
CALL apt_add_column_if_missing('gy_ykt_remainder', 'local_balance_version',
'local_balance_version BIGINT NOT NULL DEFAULT 1');
CALL apt_add_column_if_missing('gy_sb', 'face_scope_type',
'face_scope_type VARCHAR(20) NULL COMMENT ''§12 人脸设备范围''');
-- 未决项显式留痕(不做 DDL):
-- Q-20 楼栋字段启用 -> apt_master_room.building 已建但不参与筛选
-- Q-40 apt_local_task 补列冻结 -> request_payload JSON 兜底
-- Q-43 账务补列冻结 -> 04_domain_04_ledger.sql 只建结构,不开放读写
-- Q-48 平台统计表冻结 -> 平台库 pl_apt_stat_* 本轮不建(延后域)
+44
View File
@@ -0,0 +1,44 @@
-- =============================================================================
-- 可重放回填种子(DDL 与回填分离;CR-02 表 3-33 / 表 13-10 验收③)
-- 所有 INSERT 均为「不存在才插入」,重复执行不产生重复数据。
-- 执行前可用以下方式覆盖公寓标识(需与环境变量 APT_GY_ID 保持一致):
-- mysql ... -e "SET @apt_gyid='YOUR_GYID'; SOURCE 08_seed_replayable.sql"
-- =============================================================================
SET @apt_gyid := COALESCE(NULLIF(@apt_gyid, ''), 'APT-REV01-001');
-- 主数据版本基线:Mock 阶段显式标 LOCAL、source_version=0
INSERT INTO apt_master_version (gyid, domain, source_version, source, state, applied_at, remark)
SELECT @apt_gyid, d.domain_name, 0, 'LOCAL', 'APPLIED', NOW(3), 'IT-0 基线种子(非平台下发)'
FROM (
SELECT 'APARTMENT' AS domain_name UNION ALL SELECT 'ROOM' UNION ALL SELECT 'PERSON'
UNION ALL SELECT 'ROUTE' UNION ALL SELECT 'RULE' UNION ALL SELECT 'HOLIDAY'
) d
WHERE NOT EXISTS (
SELECT 1 FROM apt_master_version v WHERE v.gyid = @apt_gyid AND v.domain = d.domain_name
);
-- 上报水位初始行
INSERT INTO apt_report_watermark (gyid, current_watermark, acked_watermark)
SELECT @apt_gyid, 0, 0
WHERE NOT EXISTS (SELECT 1 FROM apt_report_watermark w WHERE w.gyid = @apt_gyid);
-- 12 项自检项定义(只登记检查项,结果由 IT-7 落库;本轮不写业务结果)
INSERT INTO apt_ops_selfcheck (gyid, check_id, check_item, result, call_ready, checked_at)
SELECT @apt_gyid, c.check_id, c.check_item, 'UNKNOWN', 0, NOW(3)
FROM (
SELECT 'IT0-01' AS check_id, 'DB_CONNECT' AS check_item UNION ALL
SELECT 'IT0-02', 'DB_READ_WRITE' UNION ALL
SELECT 'IT0-03', 'REDIS_CONNECT' UNION ALL
SELECT 'IT0-04', 'REDIS_DB_INDEX' UNION ALL
SELECT 'IT0-05', 'REDIS_READ_WRITE' UNION ALL
SELECT 'IT0-06', 'DDL_TABLE_COUNT' UNION ALL
SELECT 'IT0-07', 'DDL_PK_UK_IDX' UNION ALL
SELECT 'IT0-08', 'GYID_ISOLATION' UNION ALL
SELECT 'IT0-09', 'CREDENTIAL_SCAN' UNION ALL
SELECT 'IT0-10', 'QUARTZ_STORE' UNION ALL
SELECT 'IT0-11', 'AMI_REACHABLE' UNION ALL
SELECT 'IT0-12', 'CALL_READY'
) c
WHERE NOT EXISTS (
SELECT 1 FROM apt_ops_selfcheck s WHERE s.gyid = @apt_gyid AND s.check_id = c.check_id
);
+64
View File
@@ -0,0 +1,64 @@
-- =============================================================================
-- IT-0 验收校验脚本(表 13-10 验收③ / MS-0)
-- 预期:business_table_count = 59;missing_pk = 0;missing_gyid = 0
-- 口径:apt_* 36 + gy_*/gygl_* 18 + duty_* 5 = 59(CR-02 表 3-25、公寓端/03 表 3-8)
-- apt_ops_probe 为连通性探针表,显式排除,不计入 59 张。
-- =============================================================================
SELECT '=== 1. 业务表总数(应为 59)===' AS section;
SELECT COUNT(*) AS business_table_count
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_TYPE = 'BASE TABLE'
AND (TABLE_NAME LIKE 'apt\_%' OR TABLE_NAME LIKE 'gy\_%'
OR TABLE_NAME LIKE 'gygl\_%' OR TABLE_NAME LIKE 'duty\_%')
AND TABLE_NAME <> 'apt_ops_probe';
SELECT '=== 2. 分前缀分布(应为 apt 36 / gy 17 / gygl 1 / duty 5)===' AS section;
SELECT CASE
WHEN TABLE_NAME LIKE 'apt\_%' THEN 'apt'
WHEN TABLE_NAME LIKE 'gygl\_%' THEN 'gygl'
WHEN TABLE_NAME LIKE 'gy\_%' THEN 'gy'
ELSE 'duty'
END AS prefix, COUNT(*) AS cnt
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND (TABLE_NAME LIKE 'apt\_%' OR TABLE_NAME LIKE 'gy\_%'
OR TABLE_NAME LIKE 'gygl\_%' OR TABLE_NAME LIKE 'duty\_%')
AND TABLE_NAME <> 'apt_ops_probe'
GROUP BY prefix ORDER BY prefix;
SELECT '=== 3. 缺主键的业务表(应为 0 行)===' AS section;
SELECT t.TABLE_NAME
FROM information_schema.TABLES t
WHERE t.TABLE_SCHEMA = DATABASE()
AND (t.TABLE_NAME LIKE 'apt\_%' OR t.TABLE_NAME LIKE 'gy\_%'
OR t.TABLE_NAME LIKE 'gygl\_%' OR t.TABLE_NAME LIKE 'duty\_%')
AND t.TABLE_NAME <> 'apt_ops_probe'
AND NOT EXISTS (
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS c
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
AND c.CONSTRAINT_TYPE = 'PRIMARY KEY'
);
SELECT '=== 4. 缺 gyid 列的业务表(应为 0 行)===' AS section;
SELECT t.TABLE_NAME
FROM information_schema.TABLES t
WHERE t.TABLE_SCHEMA = DATABASE()
AND (t.TABLE_NAME LIKE 'apt\_%' OR t.TABLE_NAME LIKE 'gy\_%'
OR t.TABLE_NAME LIKE 'gygl\_%' OR t.TABLE_NAME LIKE 'duty\_%')
AND t.TABLE_NAME <> 'apt_ops_probe'
AND NOT EXISTS (
SELECT 1 FROM information_schema.COLUMNS c
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
AND c.COLUMN_NAME = 'gyid'
);
SELECT '=== 5. 外键数量(应为 0:docs/10 表 5-6 明确不建外键)===' AS section;
SELECT COUNT(*) AS foreign_key_count
FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND CONSTRAINT_TYPE = 'FOREIGN KEY';
SELECT '=== 6. 探针表与基线种子 ===' AS section;
SELECT (SELECT COUNT(*) FROM apt_ops_probe) AS probe_rows,
(SELECT COUNT(*) FROM apt_master_version WHERE source = 'LOCAL') AS local_baseline_rows,
(SELECT COUNT(*) FROM apt_ops_selfcheck) AS selfcheck_item_rows;
@@ -0,0 +1,406 @@
> **文档定位**:`IT-0 基线对齐与契约冻结` 第一轮开发的交付说明,与 `docs/公寓端/13` 表 13-10 / `DV-0` 一一对应。
> **版本**:v1.0 | 2026-11-10
> **环境**:MySQL `apt_rev01` @ `127.0.0.1:3306`(凭据由环境变量注入)、Redis 本机 `database=1`
> **上位依据**:`docs/10` §9、`公寓端/07` 表 7-1~7-8、`公寓端/03` 表 3-8~3-15、CR-02 表 3-25 / 3-27~3-33、`公寓端/13` 表 13-4 / 13-6 / 13-10 / 13-21
> **权威关系**:本文件是**实现说明**,不改变任何设计口径;阶段划分与验收仍以 `07` 为准,业务规则 / 状态 / 接口仍以 CR-02 §11~§15 为准。
---
## 1. 交付概览与口径偏离登记
表 14-1 第一轮交付物清单(对应 `DV-0`)
| 编号 | 交付物 | 落点 | 状态 |
| --- | --- | --- | --- |
| DV-0-1 | 分域增量 DDL(59 张业务表 + 1 张探针表) | `dev/sql/apt_rev01/00`~`06` | 已交付并在 `apt_rev01` 实际执行 |
| DV-0-2 | 幂等补列脚本 / 可重放种子 / 验收校验脚本 | `dev/sql/apt_rev01/07`、`08`、`99` | 已交付并二次执行验证 |
| DV-0-3 | 数据源与 Redis 连接配置、凭据外置 | `application-druid.yml`、`application.yml`、`.gitignore`、`dev/.env.example` | 已交付 |
| DV-0-4 | 基座模块(错误码 / 上下文 / 拦截器 / 异常映射 / 探针服务) | `ruoyi-common`、`ruoyi-framework` 的 `com.ruoyi.apt.**` | 已交付 |
| DV-0-5 | 连通性自检接口(非业务 Controller) | `ruoyi-admin` 的 `com.ruoyi.apt.ops.controller` | 已交付 |
| DV-0-6 | 测试骨架(JUnit5 + MockMvc + Mockito) | `ruoyi-admin/src/test/java/com/ruoyi/apt/**`、`maven-surefire-plugin` | 12 用例全绿 |
| DV-0-7 | CI 工作流(编译 / 单测 / 明文凭据扫描) | `.github/workflows/it0-verify.yml` | 已交付 |
| DV-0-8 | 本地启动脚本 | `dev/scripts/run-local.ps1` | 已交付 |
表 14-2 口径偏离与待评审项(须阶段二冻结评审签字)
| 项 | 设计口径 | 本轮实际 | 处置 |
| --- | --- | --- | --- |
| DDL 执行时点 | 待阶段二契约冻结后执行 | 按用户显式指令在 `IT-0` 预执行 | 登记偏离;脚本幂等可重放,冻结后如调整列由 `07` 增量补齐 |
| 业务 Controller | `R-02` 冻结前不写 Controller | 只写**运维自检接口**,无任何入住 / 叫乘 / 账务业务 Controller | 不算偏离;自检接口属基座连通性验证 |
| 33 张表字段 | 文档只有表名 / owner,无列级定义 | 按接口契约与表 3-9/3-15 通用规则**推断补齐**,注释标 `[推断]` | 待 `Q-20` / `Q-40` / `Q-43` 冻结评审确认 |
| 错误码 `50304` | 表 4-4 未列 | 在既有 `503xx` 段内新增「数据异常」 | 沿用 CR-02 同段追加做法,需冻结评审登记 |
| 技术基线 | 设计锁定 JDK 1.8 / Boot 2.5.15;`dev` 实测 JDK 17 / Boot 4.1.0 | 本机 `JAVA_HOME` 为 JDK 24(PATH 中另有 JDK 8),Maven 3.8.1,**实编译通过** | `IR-01` 仍为 P0 决策项,须冻结会定稿 |
| 平台统计表 | `pl_apt_stat_*` 待 `Q-48` 冻结 | 本轮**不建**(平台库不在本轮范围) | 与表 13-29 一致 |
---
## 2. 功能模块职责边界与相互依赖关系
表 14-3 模块清单
| 模块 | 代码落点 | 职责(做什么) | 边界(不做什么) | 依赖 |
| --- | --- | --- | --- | --- |
| `apt-common` | `ruoyi-common/com.ruoyi.apt.common` | `AptErrorCode` 错误码、`AptException`、`AptContext`(gyid + traceId) | 不依赖 Web / JDBC / Redis;不读配置 | 无 |
| `apt-config` | `ruoyi-framework/com.ruoyi.apt.framework.config` | `AptProperties` 绑定 `apt.*`(gyid、探针超时、key 前缀、TTL、分页上限) | 不写业务规则 | Spring Boot |
| `apt-web` | `ruoyi-framework/com.ruoyi.apt.framework.web` | `AptContextInterceptor` 注入 gyid/traceId、`AptWebMvcConfig` 注册 `/apt/**`、`AptInfraExceptionHandler` 统一异常映射 | 不采信请求参数中的 gyid;不外泄堆栈;本轮只注册 `/apt/**` | `apt-common`、`apt-config` |
| `apt-infra.connectivity` | `ruoyi-framework/com.ruoyi.apt.infra.connectivity` | `AptConnectivityService` DB/Redis 连通性 + 读写闭环、`ProbeResult` 结构化结果 | **只操作探针表 `apt_ops_probe` 与探针 key**,不碰 59 张业务表;不产生业务数据 | `DataSource`、`StringRedisTemplate`、`apt-config`、`apt-common` |
| `apt-ops` | `ruoyi-admin/com.ruoyi.apt.ops.controller` | 自检接口:参数校验、结果组装、strict 错误码、回环访问控制 | 不提供任何业务接口 | 上述全部 |
| `sql/apt_rev01` | `dev/sql/apt_rev01/*.sql` | 59 张业务表结构、幂等补列、可重放种子、验收校验 | DDL 与回填分离;不建外键;不含真实凭据 | MySQL 9.5 |
| `ci` | `.github/workflows/it0-verify.yml` | 编译 / 单测 / 明文凭据扫描卡口 | 不执行 DDL、不连生产库 | GitHub Actions |
图 14-1 模块依赖(单向,无环)
```mermaid
graph TD
A["apt-common<br/>错误码 / 上下文"] --> B["apt-config<br/>配置绑定"]
A --> C["apt-web<br/>拦截器 / 异常映射"]
B --> C
A --> D["apt-infra.connectivity<br/>DB / Redis 探针"]
B --> D
C --> E["apt-ops<br/>自检接口"]
D --> E
F["DataSource Druid<br/>MySQL apt_rev01"] --> D
G["StringRedisTemplate<br/>Redis database=1"] --> D
H["sql/apt_rev01<br/>59 张业务表"] --> F
```
表 14-4 模块间调用关系与依赖顺序
| 顺序 | 依赖对 | 约束 |
| --- | --- | --- |
| 1 | `sql` → `DataSource` | 先建表再启动应用;`SELECT 1` 可在无表时通过,但读写探针需 `apt_ops_probe` |
| 2 | `apt-common` → 全部 | 错误码与 gyid 上下文是统一异常映射的前置 |
| 3 | `apt-config` → `apt-web` / `apt-infra` | gyid 只能来自配置,不能来自请求 |
| 4 | `apt-web`(拦截器)→ `apt-ops` | 缺 gyid 时 `AptContext.getGyId()` 抛 `40302`,接口不可降级 |
| 5 | `apt-infra` → `apt-ops` | 探针失败先落 `ProbeResult`,由控制器按 `strict` 决定是否交给异常处理器 |
| 6 | `apt-web`(异常处理器)← `apt-ops` | 所有 `AptException` / `DataAccessException` 统一转 `{code,msg,data{reason,traceId,path}}` |
> **与表 13-21 的对齐**:第一轮基座(infra)落 `ruoyi-framework` 而非 `ruoyi-system`,因为 framework 已依赖 system + 数据源 + Redis,可避免反向依赖;`IT-1` 起的业务域包仍按表 13-21 落 `ruoyi-system` 的 `com.ruoyi.apt.*`。未新建任何 Maven 模块,唯一启动 jar 仍为 `ruoyi-admin`。
---
## 3. 数据表结构设计
### 3.1 总体口径
表 14-5 通用列与约束规则
| 规则 | 内容 | 依据 |
| --- | --- | --- |
| 引擎 / 字符集 | `InnoDB` + `utf8mb4` + `utf8mb4_0900_ai_ci` | MySQL 9.5 |
| 主键 | 全部使用代理主键 `id BIGINT NOT NULL AUTO_INCREMENT`,`PRIMARY KEY(id)` | CR-02 表 3-26、表 3-33 |
| 唯一键 | 业务幂等键一律 `UNIQUE KEY` 且**以 `gyid` 打头**,多租户隔离 | 公寓端/03 表 3-15、CR-02 表 3-33 |
| 外键 | **0 个**(`99_verify.sql` 第 5 项校验为 0);跨端 / 分库场景由应用层约束 + 对账兜底 | `docs/10` 表 5-6 |
| 状态列 | `VARCHAR` 大写下划线枚举 + `CHECK` 约束;不用 MySQL `ENUM`、不用数字码 | CR-02 表 3-26 |
| 金额 / 比率 / 时间 | `DECIMAL(12,2)` / `DECIMAL(10,2)` / `DATETIME(3)`;禁止浮点 | `docs/10` 表 5-4 |
| 审计列 | `deleted TINYINT NOT NULL DEFAULT 0`、`create_time`、`update_time` | 表 3-15 |
| 单写入口 | `gy_jcxx` 的 `ryzt/finish_jc/finish_ly/stoped` 仅 `apt.duty.state` 可写;`gy_rooms.room_status` 仅 `apt-stay` 可写 | CR-02 表 3-33、表 13-10 |
| 乐观锁 | `biz_version` / `state_version` / `config_version` / `balance_version`,语义互不混用 | CR-02 表 3-26 |
| 三时间戳 | `occurred_at`(业务发生)、`received_at`(本地入库)、`platform_recv_at`(归日依据) | §15 |
| 幂等 | 全部 `CREATE TABLE IF NOT EXISTS`;补列走 `apt_add_column_if_missing`;种子 `WHERE NOT EXISTS` | 表 13-10 验收③ |
### 3.2 59 张业务表清单(按域,完整字段定义见 `dev/sql/apt_rev01/*.sql`)
表 14-6 域 1:主数据 / 计划 / 本地建单(11 张,`01_domain_01_master.sql`)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 1 | `apt_master_version` | id | (gyid,domain) | (gyid,state,applied_at) | source_version, source=PLATFORM/LOCAL |
| 2 | `apt_master_apartment` | id | (gyid,apartment_code) | — | apartment_name, state |
| 3 | `apt_master_room` | id | (gyid,room_id) | (gyid,room_floor,room_no) | room_floor, building(Q-20 未启用), state_version, source_version |
| 4 | `apt_master_person` | id | (gyid,ryid) | (gyid,work_no),(gyid,xm) | photo_ref/feature_ref 只存引用, face_sync_state |
| 5 | `apt_master_route` | id | (gyid,jlid) | (gyid,cc,kcsj) | cc, kcsj, state=ACTIVE/STOPPED |
| 6 | `apt_master_rule` | id | (gyid,rule_id,rule_version) | (gyid,rule_type,state) | rule_body JSON |
| 7 | `apt_holiday` | id | (gyid,holiday_date) | — | holiday_type |
| 8 | `gy_syn_jcxx` | id | (gyid,jcid) | (gyid,jlid,jcsj) | jcsj 仅基线, computed_jcsj 权威 |
| 9 | `gy_syn_jcxx_status` | id | (gyid,jcid) | (gyid,sync_state,last_sync_at) | retry_count, error_code |
| 10 | `gy_call_duty_match_lock` | id | (gyid,lock_key) | (gyid,lease_until) | lease_until, owner_ref |
| 11 | `apt_local_task` | id | (gyid,task_id),(gyid,jcid) | (gyid,apply_id),(gyid,match_state,checkin_state) | match_state / checkin_state / sync_state 三状态链, request_payload JSON(Q-40 兜底) |
表 14-7 域 2:执行 / 设备(11 张,`02_domain_02_exec_device.sql`)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 12 | `gy_jcxx` | id | (gyid,jcid) | (gyid,jcsj),(gyid,ryzt,finish_ly),(gyid,dispatch_seq) | ryzt 0~5, finish_jc/finish_ly/stoped, sjlysj, call_mode, biz_version, 三时间戳 |
| 13 | `gy_jcxx_his` | id | (gyid,jcid) | (gyid,archived_at) | summary_payload JSON(satisfaction / roomOccupancy) |
| 14 | `gygl_ly_event` | id | (gyid,event_id) | (gyid,event_type,occurred_at) | event_type, event_source, report_state 五值 |
| 15 | `gy_rooms` | id | (gyid,room_id) | (gyid,room_status,room_floor) | room_status 1~6, state_version |
| 16 | `gy_clean_task` | id | (gyid,task_id) | (gyid,state,scheduled_at) | state, assignee_ref |
| 17 | `gy_bjxx` | id | (gyid,alarm_id) | (gyid,alarm_type,occurred_at),(gyid,handled_at) | handled_at, biz_version |
| 18 | `gy_bjcstj` | id | (gyid,stat_date) | — | alarm_count, handled_count |
| 19 | `gy_sb` | id | (gyid,device_code) | (gyid,device_type,state) | face_scope_type/ref(§12 补列) |
| 20 | `gy_sb_xt` | id | (gyid,device_code) | (gyid,online_state,heartbeat_at) | online_state |
| 21 | `gy_sb_zl` | id | (gyid,command_id) | — | 只读存量兼容 |
| 22 | `apt_device_command` | id | (gyid,command_id) | (gyid,state,create_time) | state, protocol_confirmed 默认 0 |
表 14-8 域 3:语音(12 张,`03_domain_03_voice.sql`)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 23 | `apt_sip_endpoint` | id | (gyid,endpoint_id),(gyid,extension) | (gyid,sync_state) | extension 大写, config_version, sync_state 四值 |
| 24 | `apt_sip_binding` | id | (gyid,binding_id) | (gyid,room_id,state) | effective_from/to |
| 25 | `apt_trunk` | id | (gyid,trunk_id) | — | host_ref 只存引用 |
| 26 | `apt_call_session` | id | (gyid,session_id) | (gyid,jcid),(gyid,call_state,started_at) | conf_no 不加唯一约束, answered_at |
| 27 | `apt_call_event` | id | (gyid,event_id),(gyid,session_id,event_seq) | (gyid,occurred_at) | 只追加 |
| 28 | `apt_call_event_his` | id | (gyid,event_id) | (gyid,archived_at) | 只读 |
| 29 | `apt_recording` | id | (gyid,recording_id) | (gyid,session_id) | file_ref 只存引用, state 四值 |
| 30 | `apt_tts_utterance` | id | (gyid,utterance_id) | — | audio_ref |
| 31 | `apt_ast_config_version` | id | (gyid,version_id) | — | checksum, applied_at |
| 32 | `apt_ast_drift` | id | (gyid,drift_id) | (gyid,state,detected_at) | expected_ref / actual_ref |
| 33 | `apt_ast_ami_session` | id | (gyid,ami_session_id) | (gyid,state,connected_at) | reconnect_count |
| 34 | `apt_sip_reg_status` (T-17) | id | (gyid,endpoint_id,event_seq) | (gyid,state,occurred_at),(gyid,endpoint_id,occurred_at) | 只追加、无 update_time、state 四值 / source 五值 |
表 14-9 域 4:账务(6 张,`04_domain_04_ledger.sql`,**预埋不上线**)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 35 | `gy_ykt_remainder` | id | (gyid,ryid) | — | knye/xjye 双额度, balance_version, local_balance_version |
| 36 | `gy_ykt_consume` | id | (gyid,flow_id),(gyid,device_event_id) | (gyid,occurred_at) | allowance_amount, cash_amount, ledger_state |
| 37 | `gy_ykt_allowance` | id | (gyid,allowance_id) | (gyid,ryid,issue_at) | amount, period |
| 38 | `gy_ykt_cash_request` | id | (gyid,request_id) | (gyid,request_state,create_time) | **申请表不存金额** |
| 39 | `gy_ykt_xjczjl` | id | (gyid,flow_id) | (gyid,occurred_at) | amount 正数计收入 |
| 40 | `apt_ledger_offline` (T-11) | id | (gyid,type,flow_id) | (gyid,reconcile_state,occurred_at) | balance_before/after, idempotency_key, offline_scope |
表 14-10 域 5:上报 / 值班 / 运维(11 张,`05_domain_05_report_duty_ops.sql`)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 41 | `apt_report_outbox` | id | (gyid,idempotency_key) | (gyid,state,watermark),(gyid,biz_type,biz_id) | **队列态** 五值, retry_batch_id, watermark |
| 42 | `apt_report_watermark` | id | (gyid) | — | current/acked_watermark, clock_skew_ms |
| 43 | `duty_operation_log` | id | (gyid,log_id) | (gyid,occurred_at),(gyid,trace_id) | detail JSON 兜底 |
| 44 | `duty_manual_action` | id | (gyid,action_id) | (gyid,jcid,occurred_at) | duplicated 标志 |
| 45 | `duty_shift_handover` | id | (gyid,handover_id) | (gyid,handed_at) | content JSON |
| 46 | `apt_ops_selfcheck` | id | (gyid,check_id) | (gyid,checked_at) | 12 项自检定义 |
| 47 | `apt_person_account` | id | (gyid,ryid) | — | sys_user_id |
| 48 | `apt_report_history` (T-09) | id | (gyid,idempotency_key) | (gyid,state,sent_at) | **终态流水**,与 outbox 不互换 |
| 49 | `apt_report_retry_batch` (T-10) | id | (gyid,batch_id) | — | trigger_type, from/to_watermark |
| 50 | `duty_roster` (T-12) | id | (gyid,shift_id,ryid) | (gyid,shift_date) | shift_type DAY/NIGHT |
| 51 | `duty_performance` (T-13) | id | (gyid,stat_date,ryid) | (gyid,stat_date) | 本地算、平台汇总 |
表 14-11 域 6:CR-02 新增业务表(8 张,`06_domain_06_cr02_extra.sql`)
| # | 表名 | 主键 | 唯一键(UK) | 主要索引(IDX) | 关键列 |
| --- | --- | --- | --- | --- | --- |
| 52 | `apt_tablet_device` (T-01) | id | (gyid,device_code) | — | activation_code_ref, register_status |
| 53 | `apt_tablet_task` (T-02) | id | (gyid,task_id) | (gyid,dispatch_seq),(gyid,jcsj) | dispatch_seq 单调, pull_state |
| 54 | `apt_tablet_call_result` (T-03) | id | (gyid,result_id) | (gyid,task_id),(gyid,applied_state,occurred_at) | result_id 幂等, face_verify_state |
| 55 | `apt_person_face_reg` (T-04) | id | (gyid,reg_id) | (gyid,reg_state,occurred_at),(gyid,work_no) | 双路状态链 upload_state / dispatch_state 独立, 媒体只存引用 |
| 56 | `apt_transfer_order` (T-05) | id | (gyid,transfer_id) | (gyid,jcid) | new_call_time vs recomputed_call_time 不等时告警 |
| 57 | `apt_clean_staff` (T-06) | id | (gyid,staff_id) | — | phone_ref |
| 58 | `apt_clean_record` (T-07) | id | (gyid,record_id) | (gyid,room_id,started_at) | clean_type |
| 59 | `apt_device_alarm` (T-08) | id | (gyid,alarm_id),(gyid,alarm_event_key) | (gyid,alarm_type,occurred_at) | alarm_event_key 去重 |
表 14-12 关键表示例:`apt_ops_probe`(探针专用,不计入 59 张)
| 列名 | 类型 | 约束 | 说明 |
| --- | --- | --- | --- |
| `id` | `BIGINT` | PK, AUTO_INCREMENT | 代理主键 |
| `gyid` | `VARCHAR(32)` | NOT NULL, UK 首列 | 公寓隔离标识 |
| `probe_key` | `VARCHAR(64)` | NOT NULL, UK 次列 | 探针幂等键 |
| `payload` | `VARCHAR(256)` | NULL | 读回比对值 |
| `created_at` | `DATETIME(3)` | NOT NULL DEFAULT CURRENT_TIMESTAMP(3) | 写入时刻 |
| `uk_probe_gyid_key` | — | UNIQUE (gyid, probe_key) | 重复写触发 1062 |
| `idx_probe_gyid_time` | — | INDEX (gyid, created_at) | 按公寓查残留 |
> **完整逐列定义**(含每列类型、默认值、注释、`CHECK` 表达式、`UNIQUE`/`INDEX` 定义)以 `dev/sql/apt_rev01/01`~`06` 六个脚本为唯一落点;本文件表 14-6~14-11 给出全覆盖的结构索引,避免正文重复约 900 列定义。
### 3.3 DDL 脚本、执行顺序与幂等验证
表 14-13 脚本清单(严格按 CR-02 表 3-33 迁移顺序)
| 顺序 | 文件 | 内容 | 表数 |
| --- | --- | --- | --- |
| 1 | `00_common.sql` | 幂等补列存储过程 `apt_add_column_if_missing` + 探针表 | 1(探针) |
| 2 | `01_domain_01_master.sql` | 域 1 主数据 / 计划 / 本地建单 | 11 |
| 3 | `02_domain_02_exec_device.sql` | 域 2 执行 / 设备 | 11 |
| 4 | `03_domain_03_voice.sql` | 域 3 语音 + T-17 | 12 |
| 5 | `04_domain_04_ledger.sql` | 域 4 账务 + T-11(只建结构) | 6 |
| 6 | `05_domain_05_report_duty_ops.sql` | 域 5 上报 / 值班 / 运维 + T-09/10/12/13 | 11 |
| 7 | `06_domain_06_cr02_extra.sql` | 域 6 T-01~T-08 | 8 |
| 8 | `07_patch_columns.sql` | 幂等增量补列(空库为 no-op) | — |
| 9 | `08_seed_replayable.sql` | 可重放种子:6 域 LOCAL 基线 + 水位 + 12 自检项 | — |
| 10 | `99_verify.sql` | 59 张表口径 / PK / gyid / 外键 / 种子校验 | — |
另需先导入 `dev/RuoYi-Vue-master/sql/ry_20260417.sql` + `quartz.sql`(31 张 `sys_`/`gen_`/`QRTZ_` 框架表),否则 `sys_config` 不可读、应用无法启动。最终库内 **91 张 = 59 业务 + 31 框架 + 1 探针**。
---
## 4. 接口设计
### 4.1 前缀与鉴权
| 项 | 约定 |
| --- | --- |
| 前缀 | `/apt/ops/connectivity`(四前缀中的 `/apt/**`;本轮未开 `/duty/**`、`/open/v1/**`、`/platform/**`) |
| 鉴权 | `@Anonymous` 免登录,但由 `assertAllowed` 双重限制:`apt.probe.enabled=true` 且 `remoteAddr` 为回环地址 |
| 越权 | 非回环 `40301 NON_LOOPBACK_FORBIDDEN`;接口关闭 `40301 PROBE_DISABLED`;gyid 未注入 `40302 GY_ID_MISSING` |
| 分页 | 本轮接口不分页;`apt.page.max-size=100` 为后续列表接口上限 |
| 编码 | 响应 body 为 UTF-8;`server.servlet.encoding.force-response=true` 且接口 `produces` 声明 `application/json;charset=UTF-8` |
> **编码实测说明**:服务端 HTTP body 为合法 UTF-8(用 `json.loads(resp.read().decode('utf-8'))` 复核,中文「仅支持」正确还原);Spring MVC 的 Jackson converter 会把 `Content-Type` 写成 `application/json`(不带 `charset`)。浏览器 / curl / axios 等按 UTF-8 解码正常;Windows PowerShell 5.1 的 `Invoke-RestMethod` 按 Latin-1 解码会显示中文乱码,属客户端限制,不影响 `code` / `reason` / `traceId` 等 ASCII 字段和错误处理链路。
### 4.2 `GET /apt/ops/connectivity` — 只读连通性检查
表 14-14 请求参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| 无 | — | — | — | 不接受任何业务参数;gyid 来自部署环境 |
表 14-15 返回结构
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` / `msg` | int / string | 统一响应头,成功 `200 / success` |
| `data.overall` | string | `OK` / `DEGRADED` / `FAILED` |
| `data.gyid` | string | 当前进程公寓标识 |
| `data.traceId` | string | 32 位无横线 UUID,与日志关联 |
| `data.elapsedMs` | long | 各探针耗时之和 |
| `data.results[]` | array | 每项含 `target`(DB/REDIS)、`ok`、`code`、`reason`、`message`、`elapsedMs`、`details` |
| `data.results[].details` (DB) | object | `operation=SELECT`、`select1`、`probeRows`、`gyid` |
| `data.results[].details` (REDIS) | object | `operation=PING`、`ping=PONG`、`database=1` |
### 4.3 `POST /apt/ops/connectivity/probe` — 读写闭环自检
表 14-16 请求参数(`application/json`)
| 字段 | 类型 | 必填 | 默认 | 校验 / 说明 |
| --- | --- | --- | --- | --- |
| `target` | string | 否 | `ALL` | 仅 `DB` / `REDIS` / `ALL`(大小写不敏感);其他值 `40001 INVALID_TARGET` |
| `strict` | boolean | 否 | `true` | `true`:任一失败即抛 `AptException` 走统一错误码;`false`:返回结构化降级结果 |
表 14-17 返回结构(成功)
| 字段 | 说明 |
| --- | --- |
| `data.results[]` (DB) | `operation=INSERT/SELECT/DELETE`、`inserted=1`、`readBack=it0-probe`、`deleted=1`、`gyid` |
| `data.results[]` (REDIS) | `operation=SET/GET/DEL`、`key=apt:rev01:probe:<16位>`、`readBack`、`deleted=true`、`database=1`、`ttlSeconds=60` |
| 其余字段 | 与表 14-15 相同 |
失败响应(`strict=true` 或参数非法):`{ "code": <错误码>, "msg": "...", "data": { "reason": "...", "traceId": "...", "path": "..." } }`,**不返回堆栈**。
### 4.4 错误码约定
表 14-18 错误码表(沿用 `公寓端/04` 表 4-4 分段)
| 码 | 含义 | 典型 `reason` | 触发 |
| --- | --- | --- | --- |
| 200 | 成功 | `OK` | 全部探针通过 |
| 40001 | 参数错误 | `INVALID_TARGET`、`INVALID_PROBE_TABLE` | target 非法、探针表名白名单不通过 |
| 40101 | 未登录 | — | 预留(本接口匿名) |
| 40301 | 缺少权限点 | `PROBE_DISABLED`、`NON_LOOPBACK_FORBIDDEN` | 开关关闭 / 非回环来源 |
| 40302 | gyid 越界或缺失 | `GY_ID_MISSING` | 上下文未注入 |
| 40401 | 资源不存在 | — | 预留 |
| 40901 | 业务键重复 | `DUPLICATE_KEY` | `DuplicateKeyException`(MySQL 1062) |
| 50301 | 依赖不可用 | `DB_CONNECT_FAILED`、`DB_TIMEOUT`、`DB_ERROR`、`REDIS_CONNECT_FAILED`、`REDIS_TIMEOUT`、`REDIS_ERROR`、`DEPENDENCY_TIMEOUT`、`DEPENDENCY_ERROR` | 连接失败 / 超时 / 命令异常 |
| 50304 | 数据异常(本轮同段新增) | `SCHEMA_MISMATCH`、`DATA_INTEGRITY_VIOLATION`、`DB_READ_MISMATCH`、`DB_READ_WRITE_MISMATCH`、`REDIS_DB_MISMATCH`、`REDIS_READ_MISMATCH`、`REDIS_READ_WRITE_MISMATCH` | 表缺失 / CHECK 违反 / 读写结果不符 / database≠1 |
| 500 | 内部错误 | — | 兜底 |
---
## 5. 连接、读写与异常处理策略
表 14-19 连接与超时配置
| 层 | 参数 | 值 | 作用 |
| --- | --- | --- | --- |
| JDBC URL | `connectTimeout` / `socketTimeout` | 5000 / 30000 ms | 建连与读超时快速失败 |
| Druid | `maxWait` / `maxActive` / `minIdle` | 5000 ms / 20 / 2 | 池耗尽 5s 快速失败,避免线程挂起 |
| Druid | `validationQuery=SELECT 1`、`testOnBorrow=true`、`testWhileIdle=true` | — | 借出即校验,坏连接剔除 |
| Druid | `connection-init-sqls=SET NAMES utf8mb4` | — | 字符集一致,防中文乱码 |
| Druid | 控制台 | 默认关闭 | 减少攻击面 |
| `JdbcTemplate` | `setQueryTimeout` | `max(1, apt.probe.timeout-ms/1000)` 秒 | 单次查询硬超时 |
| Redis | `timeout` / `connect-timeout` | 3s / 3s | 命令与建连超时 |
| Lettuce | `max-active` / `max-wait` | 16 / 2s | 池耗尽快速失败 |
| 探针 | `apt.probe.timeout-ms` | 2000 ms | 二次判定:即使底层未抛超时,`elapsedMs` 超限也归为 `*_TIMEOUT` |
| 探针 | `redis-ttl-seconds` | 60 s | 进程异常退出时 key 自动过期 |
| 响应 | `force-response` UTF-8 | — | 中文错误信息不乱码 |
表 14-20 异常场景矩阵
| 场景 | 底层现象 | 捕获 / 判定 | 错误码 | `reason` | 兜底与恢复 |
| --- | --- | --- | --- | --- | --- |
| DB 连接失败 | MySQL 1045 / 2003 | `CannotGetJdbcConnectionException` | 50301 | `DB_CONNECT_FAILED` | 连接池剔除坏连接;fail-fast 不回退默认口令;日志带 traceId |
| DB 超时 | 慢查询 / 网络挂起 | `QueryTimeoutException`,或 `elapsedMs > 阈值` | 50301 | `DB_TIMEOUT` / `DEPENDENCY_TIMEOUT` | `setQueryTimeout` 中断查询;`finally` 清理探针行 |
| Redis 连接失败 | 连接拒绝 / 超时 | `RedisConnectionFailureException` | 50301 | `REDIS_CONNECT_FAILED` | 3s 超时快速失败;不阻塞 DB 探针结果 |
| Redis 超时 | 命令超时 | `QueryTimeoutException` / `elapsedMs` | 50301 | `REDIS_TIMEOUT` | `spring.data.redis.timeout=3s` 兜底;key 带 TTL |
| 重复键 | MySQL 1062 | `DuplicateKeyException` | 40901 | `DUPLICATE_KEY` | 幂等键不新增行;事务回滚 |
| 完整性 / CHECK | MySQL 3819 | `DataIntegrityViolationException` | 50304 | `DATA_INTEGRITY_VIOLATION` | 事务回滚;不写半截数据 |
| 表结构缺失 | MySQL 1146 | `BadSqlGrammarException` | 50304 | `SCHEMA_MISMATCH` | 提示先执行 `00`~`06`;恢复表后自愈 |
| Redis database 错误 | `SELECT 99` 报 `ERR DB index is out of range` | `redisDatabase()` 与期望值 1 比对 | 50304 | `REDIS_DB_MISMATCH` | 拒绝写入,避免写错库 |
| 读写结果不符 | `SELECT`/`GET` 回读不一致 | 显式比对 `inserted/deleted/readBack` | 50304 | `DB_READ_WRITE_MISMATCH` / `REDIS_READ_WRITE_MISMATCH` | `finally` 二次清理 |
| 探针表名注入 | 配置被篡改 | 正则 `[A-Za-z0-9_]+` 白名单 | 40001 | `INVALID_PROBE_TABLE` | 拒绝拼接 SQL |
| gyid 缺失 | 拦截器未生效 | `AptContext.getGyId()` | 40302 | `GY_ID_MISSING` | 不降级为全库操作 |
| 清理失败 | 删除探针数据时依赖已断 | `finally` 内 `catch` 吞掉 | 不影响主结果 | 日志 WARN | 探针 key 有 TTL,探针表可重放清理 |
| 未知异常 | 其他 `DataAccessException` | 兜底 handler | 50301 | `DEPENDENCY_ERROR` | 只回 `reason` + `traceId`,不外泄堆栈 |
**不残留脏数据**:DB 探针走 `INSERT → SELECT → DELETE`,`finally` 再执行一次幂等 `DELETE`;Redis 探针走 `SET(TTL) → GET → DEL`,`finally` 再 `DEL`。实测 `probeRows=0`、key 被清除。
---
## 6. 验证结果
表 14-21 本轮实际执行的验证(全部通过)
| # | 验证项 | 手段 | 结果 |
| --- | --- | --- | --- |
| 1 | MySQL 连接(用户提供凭据) | `mysql.exe` | 成功,MySQL 9.5.0,库 `apt_rev01` |
| 2 | Redis `database=1` 连通与读写 | RESP 原生 PING/SELECT/SET/GET/DEL | `+PONG` / `+OK` / `ok` / `:1` |
| 3 | DDL 首次执行 | `00`~`08` 按序 | 9 个脚本 `EXIT=0` |
| 4 | 59 张表口径 | `99_verify.sql` | `business_table_count=59`;分布 apt 36 / gy 17 / gygl 1 / duty 5 |
| 5 | 主键 / gyid / 外键 | `99_verify.sql` | 缺 PK 0 行、缺 gyid 0 行、外键 0 个 |
| 6 | 二次执行幂等 | 全套脚本重跑后再校验 | 表数仍 59,种子仍 6 / 12,无重复 |
| 7 | 框架表 | `ry_20260417.sql` + `quartz.sql` | 91 张,`sys_config` 可读 |
| 8 | DB 读写闭环 | SQL INSERT/SELECT/DELETE | 写入、回读、删除各 1 行,残留 0 |
| 9 | 重复键异常 | 双 INSERT 同 UK | `ERROR 1062` → 映射 `40901` |
| 10 | CHECK 约束异常 | 插入非法 `state` | `ERROR 3819` → 映射 `50304` |
| 11 | 连接失败 / 拒绝 | 错误口令、错误端口 | `1045` / `2003` → 映射 `50301` |
| 12 | Redis 异常分支 | `SELECT 99`、未知命令、错误端口 | `ERR DB index is out of range` / `ERR unknown command` / 连接超时 |
| 13 | Maven 编译 | JDK 24 + Maven 3.8.1 | 7 模块 `BUILD SUCCESS` |
| 14 | 单测 | Surefire 3.5.2 + JUnit5 | `Tests run: 12, Failures: 0, Errors: 0` |
| 15 | 打包 | `spring-boot-maven-plugin` | `ruoyi-admin.jar`(90 MB) |
| 16 | 端到端 GET | 启动 jar + HTTP | `overall=OK`,DB select1=1,Redis PING=PONG,database=1 |
| 17 | 端到端 POST | `target=ALL, strict=false` | DB `1/it0-probe/1`、Redis `SET/GET/DEL` 全部 `OK` |
| 18 | 端到端参数异常 | `target=UNKNOWN` | `40001 INVALID_TARGET`,带 traceId |
| 19 | 端到端数据异常 | `DROP TABLE apt_ops_probe` | `50304 SCHEMA_MISMATCH`,`overall=FAILED` |
| 20 | 恢复自愈 | 重建表后复验 | `overall=OK`,读写闭环恢复 |
> 说明:第 16~20 项为**真实启动 Spring Boot 应用**后的 HTTP 实测,凭据通过进程环境变量注入,未写入任何仓库文件。
---
## 7. 与开发计划对应及后续衔接
表 14-22 `IT-0` 验收对照(`13` 表 13-10)
| `13` 验收项 | 本轮落地 | 结论 |
| --- | --- | --- |
| ① 基线决策与契约冻结签字 | 6 项 P0 前提已在表 14-2 登记,代码按冻结前口径实现 | **部分达成**:技术基线 / Q-20 / Q-40 / Q-43 / Q-48 仍需冻结会签字 |
| ② 仓库无明文口令 + CI 扫描 | 凭据外置 `.env`(已忽略)、CI 双规则扫描 | 达成 |
| ③ DDL 与回填分离、二次执行幂等 | 表 14-13 + 验证 #6 | 达成 |
| ④ `sys_config` 可读、应用以 local 启动无异常 | 验证 #7 / #16 | 达成 |
| ⑤ 无业务 Controller | 仅自检接口 | 达成 |
表 14-23 后续迭代衔接
| 迭代 | 本轮提供的前置 | 下一轮要做 |
| --- | --- | --- |
| `IT-1` M0 运行时基座 | 错误码 / gyid 上下文 / 四前缀中 `/apt/**` 拦截器骨架 / 测试与 CI | 补齐 `/duty/**`、`/open/v1/**` 拦截器与权限点种子、`sys_menu` 权限点、审计切面 |
| `IT-2`~`IT-3` | 59 张表结构、幂等补列脚本 | 主数据 / 房间 / 人脸最小闭环业务代码(落 `ruoyi-system`) |
| `IT-4`~`IT-5` | `apt_local_task` / `gy_jcxx` 结构、单一状态写入口约束 | 入住 CI-01~CI-05、叫乘状态机 M3 |
| `IT-9`~`IT-11` 延后域 | 账务 6 张表**已建结构但不开放读写**、错误码与权限点可预埋 | 阶段三退出 + `D1/D2/P-07/Q-41~Q-45` 决策后再开 `/apt/ledger/**` |
---
## 8. 变更点与影响范围
| 类别 | 变更 | 影响范围 |
| --- | --- | --- |
| 新增 | `dev/sql/apt_rev01/*.sql`(10 个脚本) | 数据库 `apt_rev01`:59 业务表 + 1 探针表 |
| 新增 | `com.ruoyi.apt.**` 共 9 个 Java 类(common 3 / framework 5 / admin 1) | 后端编译与运行时;不改任何既有 `com.ruoyi.*` 类 |
| 新增 | 3 个测试类 + `maven-surefire-plugin` 3.5.2 + `spring-boot-starter-test` | `ruoyi-admin` 构建;修正了原 `surefire 2.12.4` 静默漏跑 JUnit5 用例的问题 |
| 修改 | `application-druid.yml`、`application.yml` | 数据源指向 `apt_rev01`、Redis `database=1`、springdoc 扫 `com.ruoyi.apt.**`、凭据全部外置 |
| 新增 | `.gitignore`、`dev/.env.example`、`dev/scripts/run-local.ps1`、`.github/workflows/it0-verify.yml` | 仓库级;不改动既有文件 |
| 新增 | 本文档 `公寓端/14` | 文档索引 |
| **不变** | CR-02 §12~§15 权威表述、跨端 20 编号、`v2-draft`、公寓本地库 59 张表、平台零回调、统计结果不回写、`F-A`~`F-Q`、Q-01~Q-50 | 无任何设计口径改动;未新增 `M7` / 阶段六 / `D-4` / `Q-*` / `R-*` / 施工单编号 |
| **待办** | 表 14-2 的 6 项口径偏离须评审签字;`IR-01` 技术基线冲突须冻结会定稿 | `IT-1` 开工前 |
+2
View File
@@ -32,6 +32,7 @@
| `11-人员入住作业流程.md` | **人员入住作业流程**:注册与签到、入住申请(房间 / 交路)、值班员审核与房间调整、#9 提报与平台运安匹配、#7 任务查询进入叫乘;三条状态链、`apt_local_task` 补列、N-38~N-47 请求 / 响应字段、Q-36~Q-40、表 11-1~11-35 | CR-02 §13(CR-C26~CR-C30、F-K、验收 18~20) |
| `12-消费充值与菜品管理.md` | **消费、充值与菜品管理**:人员消费(选餐加菜显示金额 → 查双额度 → 补贴优先 / 现金补足 → 上报并同步平台账户)、人员线下充值(扫码识别 → 管理员核对 → 计入现金额度 → 同步平台)、公寓菜品管理(阶段性食谱发布 → 终端查看并加入选择菜单 → 远程点菜预留);四条状态链、`gy_ykt_*` / `YKT_MEALS` 补列、N-48~N-58 请求 / 响应字段、Q-41~Q-45、表 12-1~12-34 | CR-02 §14(CR-C31~CR-C35、F-L / F-M / F-N、验收 21~23) |
| `13-公寓端小步迭代开发计划.md` | **实施排期设计稿(非设计变更)**:`dev/` 实测上游、`IT-0`~`IT-8` 迭代目标 / 范围 / 依赖 / 验收、里程碑 `MS-0`~`MS-5`、交付物 `DV-0`~`DV-8`、设计文档 × 代码模块对齐矩阵、6 项 P0 排期前提与 `IR-01`~`IR-14` 风险台账、跨迭代联调与回归基线、`IT-9`~`IT-11` 消费 / 充值 / 菜品衔接;表 13-1~13-32、图 13-1 | 开发排期与联调(**阶段 / 验收口径仍以 `07` 为准**) |
| `14-第一轮开发(IT-0)交付说明.md` | **第一轮实现交付说明**:模块职责边界与依赖图、59 张表全量字段 / 主键 / 唯一键 / 索引设计、DDL 脚本与迁移顺序、自检接口的请求参数 / 返回结构 / 错误码约定、连接失败 / 超时 / 数据异常处理矩阵、20 项实测验证结果、口径偏离与后续衔接;表 14-1~14-23、图 14-1 | `IT-0` 落地与验收(代码在 `dev/RuoYi-Vue-master`、`dev/sql/apt_rev01`) |
| `../设计变更/03-三端功能设计修订(CR-02).md` | **权威增量**:三端覆盖范围、交互协议、交互数据、模块与数据表、关键流程变更、施工单 | CR-02 |
## 1. 编号与引用约定
@@ -104,6 +105,7 @@ flowchart LR
K --> L["11 人员入住作业流程"]
L --> M["12 消费、充值与菜品管理"]
M --> N["13 小步迭代开发计划(排期 / 联调)"]
N --> O["14 第一轮开发 IT-0 交付说明(实现 / 验收)"]
```
图 0-1 阅读路径
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "docs"
},
{
"path": "dev"
}
],
"settings": {}
}