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