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();
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java
new file mode 100644
index 0000000..5dac59d
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java
@@ -0,0 +1,55 @@
+package com.ruoyi.apt.common;
+
+/**
+ * 公寓端统一错误码(沿用 公寓端/04 表 4-4 分段,不新造号段)。
+ *
+ * 第一轮新增 50304「数据异常」落在既有 503xx 段内(与 CR-02 在同一分段内
+ * 追加 50303 的做法一致),需阶段二契约冻结评审登记后方可对外启用。
+ *
+ * @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;
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java
new file mode 100644
index 0000000..7251969
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java
@@ -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;
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java
new file mode 100644
index 0000000..cd033c4
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java
@@ -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;
+ }
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java
new file mode 100644
index 0000000..46c3873
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java
@@ -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();
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java
new file mode 100644
index 0000000..2a2fb94
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java
@@ -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 data = new LinkedHashMap<>();
+ data.put("reason", reason);
+ data.put("traceId", AptContext.getTraceId());
+ data.put("path", request.getRequestURI());
+ return new AjaxResult(code.getCode(), message, data);
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java
new file mode 100644
index 0000000..1d3095f
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java
@@ -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/**");
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java
new file mode 100644
index 0000000..c403971
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java
@@ -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 基座,不触碰任何业务表)。
+ *
+ * 异常分类:连接失败 -> 50301 + DB_CONNECT_FAILED / REDIS_CONNECT_FAILED;
+ * 超时 -> 50301 + *_TIMEOUT;重复键 -> 40901;完整性 / 读写结果不符 -> 50304。
+ * JDBC 查询超时由 JdbcTemplate#setQueryTimeout 从 apt.probe.timeout-ms 推导,
+ * Redis 命令超时由 spring.data.redis.timeout 统一兜底,探针再按 elapsedMs 二次判定。
+ *
+ * @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) 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);
+ }
+}
diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/ProbeResult.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/ProbeResult.java
new file mode 100644
index 0000000..15865c2
--- /dev/null
+++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/ProbeResult.java
@@ -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 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 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 toMap()
+ {
+ Map 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;
+ }
+}
diff --git a/dev/scripts/run-local.ps1 b/dev/scripts/run-local.ps1
new file mode 100644
index 0000000..1f276d4
--- /dev/null
+++ b/dev/scripts/run-local.ps1
@@ -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
diff --git a/dev/sql/apt_rev01/00_common.sql b/dev/sql/apt_rev01/00_common.sql
new file mode 100644
index 0000000..a256d77
--- /dev/null
+++ b/dev/sql/apt_rev01/00_common.sql
@@ -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 连通性探针表(非业务表)';
diff --git a/dev/sql/apt_rev01/01_domain_01_master.sql b/dev/sql/apt_rev01/01_domain_01_master.sql
new file mode 100644
index 0000000..662d7bb
--- /dev/null
+++ b/dev/sql/apt_rev01/01_domain_01_master.sql
@@ -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 行)';
diff --git a/dev/sql/apt_rev01/02_domain_02_exec_device.sql b/dev/sql/apt_rev01/02_domain_02_exec_device.sql
new file mode 100644
index 0000000..889530a
--- /dev/null
+++ b/dev/sql/apt_rev01/02_domain_02_exec_device.sql
@@ -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 补列)';
diff --git a/dev/sql/apt_rev01/03_domain_03_voice.sql b/dev/sql/apt_rev01/03_domain_03_voice.sql
new file mode 100644
index 0000000..f40178e
--- /dev/null
+++ b/dev/sql/apt_rev01/03_domain_03_voice.sql
@@ -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 分机注册状态流水(只追加)';
diff --git a/dev/sql/apt_rev01/04_domain_04_ledger.sql b/dev/sql/apt_rev01/04_domain_04_ledger.sql
new file mode 100644
index 0000000..09536c0
--- /dev/null
+++ b/dev/sql/apt_rev01/04_domain_04_ledger.sql
@@ -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 断网本地账务流水(延后域)';
diff --git a/dev/sql/apt_rev01/05_domain_05_report_duty_ops.sql b/dev/sql/apt_rev01/05_domain_05_report_duty_ops.sql
new file mode 100644
index 0000000..872f272
--- /dev/null
+++ b/dev/sql/apt_rev01/05_domain_05_report_duty_ops.sql
@@ -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 盯控质量考核';
diff --git a/dev/sql/apt_rev01/06_domain_06_cr02_extra.sql b/dev/sql/apt_rev01/06_domain_06_cr02_extra.sql
new file mode 100644
index 0000000..1721786
--- /dev/null
+++ b/dev/sql/apt_rev01/06_domain_06_cr02_extra.sql
@@ -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 设备报警';
diff --git a/dev/sql/apt_rev01/07_patch_columns.sql b/dev/sql/apt_rev01/07_patch_columns.sql
new file mode 100644
index 0000000..f007d02
--- /dev/null
+++ b/dev/sql/apt_rev01/07_patch_columns.sql
@@ -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_* 本轮不建(延后域)
diff --git a/dev/sql/apt_rev01/08_seed_replayable.sql b/dev/sql/apt_rev01/08_seed_replayable.sql
new file mode 100644
index 0000000..7c5ed14
--- /dev/null
+++ b/dev/sql/apt_rev01/08_seed_replayable.sql
@@ -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
+);
diff --git a/dev/sql/apt_rev01/99_verify.sql b/dev/sql/apt_rev01/99_verify.sql
new file mode 100644
index 0000000..9ca3127
--- /dev/null
+++ b/dev/sql/apt_rev01/99_verify.sql
@@ -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;
diff --git a/docs/公寓端/14-第一轮开发(IT-0)交付说明.md b/docs/公寓端/14-第一轮开发(IT-0)交付说明.md
new file mode 100644
index 0000000..91dc887
--- /dev/null
+++ b/docs/公寓端/14-第一轮开发(IT-0)交付说明.md
@@ -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
错误码 / 上下文"] --> B["apt-config
配置绑定"]
+ A --> C["apt-web
拦截器 / 异常映射"]
+ B --> C
+ A --> D["apt-infra.connectivity
DB / Redis 探针"]
+ B --> D
+ C --> E["apt-ops
自检接口"]
+ D --> E
+ F["DataSource Druid
MySQL apt_rev01"] --> D
+ G["StringRedisTemplate
Redis database=1"] --> D
+ H["sql/apt_rev01
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` 开工前 |
diff --git a/docs/公寓端/README.md b/docs/公寓端/README.md
index 3cc2d67..75b3aa0 100644
--- a/docs/公寓端/README.md
+++ b/docs/公寓端/README.md
@@ -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 阅读路径
diff --git a/jcgl-dev.code-workspace b/jcgl-dev.code-workspace
new file mode 100644
index 0000000..b7165cf
--- /dev/null
+++ b/jcgl-dev.code-workspace
@@ -0,0 +1,11 @@
+{
+ "folders": [
+ {
+ "path": "docs"
+ },
+ {
+ "path": "dev"
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file