From 8ffeb59dd46dd154fd0bb1ad5c383605e19859b8 Mon Sep 17 00:00:00 2001 From: jzf Date: Tue, 10 Nov 2026 14:52:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E9=98=B6=E6=AE=B5=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=8C=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/it0-verify.yml | 41 ++ .gitignore | 26 ++ dev/.env.example | 22 + dev/RuoYi-Vue-master/ruoyi-admin/pom.xml | 13 + .../controller/AptConnectivityController.java | 174 ++++++++ .../src/main/resources/application-druid.yml | 77 ++-- .../src/main/resources/application.yml | 113 ++--- .../com/ruoyi/apt/common/AptContextTest.java | 47 ++ .../AptInfraExceptionHandlerTest.java | 61 +++ .../ops/AptConnectivityControllerTest.java | 116 +++++ .../java/com/ruoyi/apt/common/AptContext.java | 65 +++ .../com/ruoyi/apt/common/AptErrorCode.java | 55 +++ .../com/ruoyi/apt/common/AptException.java | 38 ++ .../apt/framework/config/AptProperties.java | 126 ++++++ .../framework/web/AptContextInterceptor.java | 40 ++ .../web/AptInfraExceptionHandler.java | 111 +++++ .../apt/framework/web/AptWebMvcConfig.java | 27 ++ .../connectivity/AptConnectivityService.java | 349 +++++++++++++++ .../apt/infra/connectivity/ProbeResult.java | 96 +++++ dev/scripts/run-local.ps1 | 26 ++ dev/sql/apt_rev01/00_common.sql | 50 +++ dev/sql/apt_rev01/01_domain_01_master.sql | 260 +++++++++++ .../apt_rev01/02_domain_02_exec_device.sql | 254 +++++++++++ dev/sql/apt_rev01/03_domain_03_voice.sql | 227 ++++++++++ dev/sql/apt_rev01/04_domain_04_ledger.sql | 145 +++++++ .../05_domain_05_report_duty_ops.sql | 215 ++++++++++ dev/sql/apt_rev01/06_domain_06_cr02_extra.sql | 196 +++++++++ dev/sql/apt_rev01/07_patch_columns.sql | 41 ++ dev/sql/apt_rev01/08_seed_replayable.sql | 44 ++ dev/sql/apt_rev01/99_verify.sql | 64 +++ docs/公寓端/14-第一轮开发(IT-0)交付说明.md | 406 ++++++++++++++++++ docs/公寓端/README.md | 2 + jcgl-dev.code-workspace | 11 + 33 files changed, 3427 insertions(+), 111 deletions(-) create mode 100644 .github/workflows/it0-verify.yml create mode 100644 .gitignore create mode 100644 dev/.env.example create mode 100644 dev/RuoYi-Vue-master/ruoyi-admin/src/main/java/com/ruoyi/apt/ops/controller/AptConnectivityController.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/common/AptContextTest.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/framework/AptInfraExceptionHandlerTest.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/ops/AptConnectivityControllerTest.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptContext.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java create mode 100644 dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/ProbeResult.java create mode 100644 dev/scripts/run-local.ps1 create mode 100644 dev/sql/apt_rev01/00_common.sql create mode 100644 dev/sql/apt_rev01/01_domain_01_master.sql create mode 100644 dev/sql/apt_rev01/02_domain_02_exec_device.sql create mode 100644 dev/sql/apt_rev01/03_domain_03_voice.sql create mode 100644 dev/sql/apt_rev01/04_domain_04_ledger.sql create mode 100644 dev/sql/apt_rev01/05_domain_05_report_duty_ops.sql create mode 100644 dev/sql/apt_rev01/06_domain_06_cr02_extra.sql create mode 100644 dev/sql/apt_rev01/07_patch_columns.sql create mode 100644 dev/sql/apt_rev01/08_seed_replayable.sql create mode 100644 dev/sql/apt_rev01/99_verify.sql create mode 100644 docs/公寓端/14-第一轮开发(IT-0)交付说明.md create mode 100644 jcgl-dev.code-workspace diff --git a/.github/workflows/it0-verify.yml b/.github/workflows/it0-verify.yml new file mode 100644 index 0000000..f9f04a3 --- /dev/null +++ b/.github/workflows/it0-verify.yml @@ -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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67399fd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/dev/.env.example b/dev/.env.example new file mode 100644 index 0000000..2c56153 --- /dev/null +++ b/dev/.env.example @@ -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= diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/pom.xml b/dev/RuoYi-Vue-master/ruoyi-admin/pom.xml index bdaf7f6..a4e5736 100644 --- a/dev/RuoYi-Vue-master/ruoyi-admin/pom.xml +++ b/dev/RuoYi-Vue-master/ruoyi-admin/pom.xml @@ -54,6 +54,13 @@ ruoyi-generator + + + org.springframework.boot + spring-boot-starter-test + test + + @@ -72,6 +79,12 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + org.apache.maven.plugins maven-war-plugin diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/main/java/com/ruoyi/apt/ops/controller/AptConnectivityController.java b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/java/com/ruoyi/apt/ops/controller/AptConnectivityController.java new file mode 100644 index 0000000..e287e76 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/java/com/ruoyi/apt/ops/controller/AptConnectivityController.java @@ -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)。 + * + *

按 R-02「冻结前不写 Controller」,本轮不提供任何入住 / 叫乘 / 账务业务接口; + * 本接口只用于验证数据库与 Redis 的连接、读写与异常链路。匿名访问受双重限制: + * 必须 apt.probe.enabled=true 且来源为回环地址。

+ * + * @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 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 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 results, boolean strict) + { + boolean allOk = true; + boolean anyOk = false; + long totalMs = 0; + List> 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 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; + } + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml index 426a48a..c8fc1ec 100644 --- a/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml @@ -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 \ No newline at end of file + 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:} diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml index b5f1286..7206ff3 100644 --- a/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml @@ -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 diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/common/AptContextTest.java b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/common/AptContextTest.java new file mode 100644 index 0000000..38b524b --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/common/AptContextTest.java @@ -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); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/framework/AptInfraExceptionHandlerTest.java b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/framework/AptInfraExceptionHandlerTest.java new file mode 100644 index 0000000..aed5f87 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/framework/AptInfraExceptionHandlerTest.java @@ -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 data = (Map) result.get(AjaxResult.DATA_TAG); + assertEquals(reason, data.get("reason")); + assertNotNull(data.get("traceId"), "错误响应必须带 traceId"); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/ops/AptConnectivityControllerTest.java b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/ops/AptConnectivityControllerTest.java new file mode 100644 index 0000000..32bbf69 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-admin/src/test/java/com/ruoyi/apt/ops/AptConnectivityControllerTest.java @@ -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")); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptContext.java b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptContext.java new file mode 100644 index 0000000..d17e337 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptContext.java @@ -0,0 +1,65 @@ +package com.ruoyi.apt.common; + +import java.util.UUID; + +/** + * 公寓端运行时上下文:gyid 与 traceId。 + * + *

D5「一进程一 gyid」:gyid 只从部署环境注入(拦截器写入), + * 绝不接受请求参数;所有业务 SQL 必须带 gyid。traceId 用于错误响应 + * 与日志关联,避免把堆栈外泄给调用方。

+ * + * @author apt-rev01 + */ +public final class AptContext +{ + private static final ThreadLocal GY_ID = new ThreadLocal<>(); + private static final ThreadLocal TRACE_ID = new ThreadLocal<>(); + + private AptContext() + { + } + + public static void setGyId(String gyId) + { + GY_ID.set(gyId); + } + + /** 取 gyid;未注入视为隔离失效,直接拒绝(40302) */ + public static String getGyId() + { + String value = GY_ID.get(); + if (value == null || value.isEmpty()) + { + throw new AptException(AptErrorCode.FORBIDDEN_GYID, "GY_ID_MISSING", "公寓标识未注入"); + } + return value; + } + + public static String getGyIdOrNull() + { + return GY_ID.get(); + } + + public static void initTraceId() + { + TRACE_ID.set(UUID.randomUUID().toString().replace("-", "")); + } + + public static String getTraceId() + { + String value = TRACE_ID.get(); + if (value == null) + { + value = UUID.randomUUID().toString().replace("-", ""); + TRACE_ID.set(value); + } + return value; + } + + public static void clear() + { + GY_ID.remove(); + TRACE_ID.remove(); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java new file mode 100644 index 0000000..5dac59d --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptErrorCode.java @@ -0,0 +1,55 @@ +package com.ruoyi.apt.common; + +/** + * 公寓端统一错误码(沿用 公寓端/04 表 4-4 分段,不新造号段)。 + * + *

第一轮新增 50304「数据异常」落在既有 503xx 段内(与 CR-02 在同一分段内 + * 追加 50303 的做法一致),需阶段二契约冻结评审登记后方可对外启用。

+ * + * @author apt-rev01 + */ +public enum AptErrorCode +{ + SUCCESS(200, "success"), + BAD_REQUEST(40001, "参数错误"), + UNAUTHORIZED(40101, "未登录"), + FORBIDDEN_PERM(40301, "缺少权限点"), + FORBIDDEN_GYID(40302, "gyid 越界或缺失"), + NOT_FOUND(40401, "资源不存在"), + DUPLICATE_KEY(40901, "业务键重复"), + DEPENDENCY_UNAVAILABLE(50301, "依赖不可用"), + /** 数据异常:完整性约束、读写结果不符、序列化失败 */ + DATA_ANOMALY(50304, "数据异常"), + INTERNAL_ERROR(500, "内部错误"); + + private final int code; + private final String message; + + AptErrorCode(int code, String message) + { + this.code = code; + this.message = message; + } + + public int getCode() + { + return code; + } + + public String getMessage() + { + return message; + } + + public static AptErrorCode fromCode(int code) + { + for (AptErrorCode value : values()) + { + if (value.code == code) + { + return value; + } + } + return INTERNAL_ERROR; + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java new file mode 100644 index 0000000..7251969 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-common/src/main/java/com/ruoyi/apt/common/AptException.java @@ -0,0 +1,38 @@ +package com.ruoyi.apt.common; + +/** + * 带错误码与 reason 的公寓端异常,由统一异常处理器映射为 HTTP 响应。 + * + * @author apt-rev01 + */ +public class AptException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + private final AptErrorCode errorCode; + private final String reason; + + public AptException(AptErrorCode errorCode, String reason, String message) + { + super(message); + this.errorCode = errorCode; + this.reason = reason; + } + + public AptException(AptErrorCode errorCode, String reason, String message, Throwable cause) + { + super(message, cause); + this.errorCode = errorCode; + this.reason = reason; + } + + public AptErrorCode getErrorCode() + { + return errorCode; + } + + public String getReason() + { + return reason; + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java new file mode 100644 index 0000000..cd033c4 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/config/AptProperties.java @@ -0,0 +1,126 @@ +package com.ruoyi.apt.framework.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 公寓端基座配置(application.yml 的 apt.* 段)。 + * + * @author apt-rev01 + */ +@Component +@ConfigurationProperties(prefix = "apt") +public class AptProperties +{ + /** 一进程一 gyid(D5),只从部署环境注入 */ + private String gyId; + + private Probe probe = new Probe(); + private Page page = new Page(); + + public String getGyId() + { + return gyId; + } + + public void setGyId(String gyId) + { + this.gyId = gyId; + } + + public Probe getProbe() + { + return probe; + } + + public void setProbe(Probe probe) + { + this.probe = probe; + } + + public Page getPage() + { + return page; + } + + public void setPage(Page page) + { + this.page = page; + } + + public static class Probe + { + private boolean enabled = true; + /** 单次探针超时判定阈值(毫秒) */ + private long timeoutMs = 2000; + private String redisKeyPrefix = "apt:rev01:"; + private int redisTtlSeconds = 60; + /** 探针专用表,不触碰任何业务表 */ + private String table = "apt_ops_probe"; + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public long getTimeoutMs() + { + return timeoutMs; + } + + public void setTimeoutMs(long timeoutMs) + { + this.timeoutMs = timeoutMs; + } + + public String getRedisKeyPrefix() + { + return redisKeyPrefix; + } + + public void setRedisKeyPrefix(String redisKeyPrefix) + { + this.redisKeyPrefix = redisKeyPrefix; + } + + public int getRedisTtlSeconds() + { + return redisTtlSeconds; + } + + public void setRedisTtlSeconds(int redisTtlSeconds) + { + this.redisTtlSeconds = redisTtlSeconds; + } + + public String getTable() + { + return table; + } + + public void setTable(String table) + { + this.table = table; + } + } + + public static class Page + { + private int maxSize = 100; + + public int getMaxSize() + { + return maxSize; + } + + public void setMaxSize(int maxSize) + { + this.maxSize = maxSize; + } + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java new file mode 100644 index 0000000..46c3873 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptContextInterceptor.java @@ -0,0 +1,40 @@ +package com.ruoyi.apt.framework.web; + +import com.ruoyi.apt.common.AptContext; +import com.ruoyi.apt.framework.config.AptProperties; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +/** + * 把部署环境注入的 gyid 与 traceId 放入线程上下文。 + * 仅作用于 /apt/**,且只从配置读取,绝不采信请求参数。 + * + * @author apt-rev01 + */ +@Component +public class AptContextInterceptor implements HandlerInterceptor +{ + private final AptProperties properties; + + public AptContextInterceptor(AptProperties properties) + { + this.properties = properties; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + { + AptContext.initTraceId(); + AptContext.setGyId(properties.getGyId()); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) + { + AptContext.clear(); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java new file mode 100644 index 0000000..2a2fb94 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptInfraExceptionHandler.java @@ -0,0 +1,111 @@ +package com.ruoyi.apt.framework.web; + +import java.util.LinkedHashMap; +import java.util.Map; +import com.ruoyi.apt.common.AptContext; +import com.ruoyi.apt.common.AptErrorCode; +import com.ruoyi.apt.common.AptException; +import com.ruoyi.common.core.domain.AjaxResult; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 基础设施异常统一映射:连接失败 / 超时 / 数据异常 -> 约定错误码 + reason + traceId。 + * 不向调用方外泄堆栈,只写日志(公寓端/04 表 4-4、表 13-11 M0 验收)。 + * + * @author apt-rev01 + */ +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class AptInfraExceptionHandler +{ + private static final Logger log = LoggerFactory.getLogger(AptInfraExceptionHandler.class); + + @ExceptionHandler(AptException.class) + public AjaxResult handleAptException(AptException e, HttpServletRequest request) + { + log.error("apt infra error, uri={}, code={}, reason={}, traceId={}", + request.getRequestURI(), e.getErrorCode().getCode(), e.getReason(), + AptContext.getTraceId(), e); + return build(e.getErrorCode(), e.getReason(), e.getMessage(), request); + } + + @ExceptionHandler(DuplicateKeyException.class) + public AjaxResult handleDuplicateKey(DuplicateKeyException e, HttpServletRequest request) + { + return handle(AptErrorCode.DUPLICATE_KEY, "DUPLICATE_KEY", "业务键重复", e, request); + } + + @ExceptionHandler(CannotGetJdbcConnectionException.class) + public AjaxResult handleDbConnectFailure(CannotGetJdbcConnectionException e, HttpServletRequest request) + { + return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DB_CONNECT_FAILED", "数据库连接失败", e, request); + } + + @ExceptionHandler(QueryTimeoutException.class) + public AjaxResult handleTimeout(QueryTimeoutException e, HttpServletRequest request) + { + return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DEPENDENCY_TIMEOUT", "依赖访问超时", e, request); + } + + @ExceptionHandler(BadSqlGrammarException.class) + public AjaxResult handleSchemaMismatch(BadSqlGrammarException e, HttpServletRequest request) + { + return handle(AptErrorCode.DATA_ANOMALY, "SCHEMA_MISMATCH", "表结构与代码不匹配", e, request); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public AjaxResult handleIntegrity(DataIntegrityViolationException e, HttpServletRequest request) + { + return handle(AptErrorCode.DATA_ANOMALY, "DATA_INTEGRITY_VIOLATION", "数据完整性约束被破坏", e, request); + } + + @ExceptionHandler(RedisConnectionFailureException.class) + public AjaxResult handleRedisConnectFailure(RedisConnectionFailureException e, HttpServletRequest request) + { + return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "REDIS_CONNECT_FAILED", "Redis 连接失败", e, request); + } + + @ExceptionHandler(RedisSystemException.class) + public AjaxResult handleRedisSystem(RedisSystemException e, HttpServletRequest request) + { + return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "REDIS_ERROR", "Redis 命令执行异常", e, request); + } + + /** JDBC / Redis 的兜底数据访问异常 */ + @ExceptionHandler(DataAccessException.class) + public AjaxResult handleDataAccess(DataAccessException e, HttpServletRequest request) + { + return handle(AptErrorCode.DEPENDENCY_UNAVAILABLE, "DEPENDENCY_ERROR", "依赖访问异常", e, request); + } + + private AjaxResult handle(AptErrorCode code, String reason, String message, + Exception e, HttpServletRequest request) + { + log.error("apt infra error, uri={}, code={}, reason={}, traceId={}", + request.getRequestURI(), code.getCode(), reason, AptContext.getTraceId(), e); + return build(code, reason, message, request); + } + + private AjaxResult build(AptErrorCode code, String reason, String message, HttpServletRequest request) + { + Map data = new LinkedHashMap<>(); + data.put("reason", reason); + data.put("traceId", AptContext.getTraceId()); + data.put("path", request.getRequestURI()); + return new AjaxResult(code.getCode(), message, data); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java new file mode 100644 index 0000000..1d3095f --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/framework/web/AptWebMvcConfig.java @@ -0,0 +1,27 @@ +package com.ruoyi.apt.framework.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * /apt/** 前缀的基座拦截器注册(四前缀拦截器最小集,IT-1 再补 /duty/** 与 /open/**)。 + * + * @author apt-rev01 + */ +@Configuration +public class AptWebMvcConfig implements WebMvcConfigurer +{ + private final AptContextInterceptor aptContextInterceptor; + + public AptWebMvcConfig(AptContextInterceptor aptContextInterceptor) + { + this.aptContextInterceptor = aptContextInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(aptContextInterceptor).addPathPatterns("/apt/**"); + } +} diff --git a/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java new file mode 100644 index 0000000..c403971 --- /dev/null +++ b/dev/RuoYi-Vue-master/ruoyi-framework/src/main/java/com/ruoyi/apt/infra/connectivity/AptConnectivityService.java @@ -0,0 +1,349 @@ +package com.ruoyi.apt.infra.connectivity; + +import java.time.Duration; +import java.util.UUID; +import javax.sql.DataSource; +import com.ruoyi.apt.common.AptContext; +import com.ruoyi.apt.common.AptErrorCode; +import com.ruoyi.apt.common.AptException; +import com.ruoyi.apt.framework.config.AptProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.CannotGetJdbcConnectionException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * 数据库 / Redis 连通性与读写闭环探针(IT-0 基座,不触碰任何业务表)。 + * + *

异常分类:连接失败 -> 50301 + DB_CONNECT_FAILED / REDIS_CONNECT_FAILED; + * 超时 -> 50301 + *_TIMEOUT;重复键 -> 40901;完整性 / 读写结果不符 -> 50304。 + * JDBC 查询超时由 JdbcTemplate#setQueryTimeout 从 apt.probe.timeout-ms 推导, + * Redis 命令超时由 spring.data.redis.timeout 统一兜底,探针再按 elapsedMs 二次判定。

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