diff --git a/README.md b/README.md index 772973da..f099ba5d 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ smart/ | 目录 | 责任 | 当前技术与声明版本 | | --- | --- | --- | | [`smart/`](smart/README.md) | 基础平台后端,提供网关、OAuth2 认证、UPMS 和公共组件。 | Java 8、Maven、Spring Boot `2.1.3.RELEASE`、Spring Cloud `Greenwich.RELEASE`(见 [`smart/pom.xml`](smart/pom.xml)) | -| [`smart-module/`](smart-module/README.md) | 业务微服务聚合工程,包含 App、平台、数据、桥接、算法、推送、调度等模块。 | Java 8、Spring Cloud 业务服务、Maven;聚合关系见 [`smart-module/pom.xml`](smart-module/pom.xml) | +| [`smart-module/`](smart-module/README.md) | 业务微服务聚合工程,包含 App、平台、数据、门锁领域、设备桥、算法、推送、调度等模块。 | Java 8、Spring Cloud 业务服务、Maven;聚合关系见 [`smart-module/pom.xml`](smart-module/pom.xml) | | [`smart-ui/`](smart-ui/README.md) | 管理端中后台 SPA,对接网关、认证、UPMS 和园区业务 API。 | Vue `^2.7.16`、Element UI `^2.4.11`、Avue、Vue CLI、pnpm `11.3.0`(见 [`smart-ui/package.json`](smart-ui/package.json)) | | [`smart-print-renderer/`](smart-print-renderer/README.md) | 独立单面模板及双面组合的私有 PDF 渲染;不读取人员库或调用打印机。 | Node.js 24、pdfme `6.1.12` | | [`smart-print-client/`](smart-print-client/README.md) | 独立设备身份领取任务、持久命令去重、手动/自动厂牌及单面访客适配;实机能力按档案验收。 | .NET 10、Windows 官方驱动、Brother b-PAC | @@ -80,6 +80,8 @@ smart/ | `smart-algorithm` | 算法模块。 | | `smart-tool` | 智慧园区服务公共模块。 | | `smart-bridge` | 设备桥接。 | +| [`smart-lock`](smart-module/smart-lock/README.md) | 门锁领域模块;当前开发范围为 Maven 基础、内部契约与离线校验,无可运行服务或数据库接入。 | +| [`smart-bridge-lock`](smart-module/smart-bridge-lock/README.md) | 门锁协议桥模块;当前开发范围为投递/回执契约,无网络监听和真实发送能力。 | | `smart-bridge-isc` | ISC(综合安防平台)集成桥接。 | | `smart-bridge-concentrator` | 设备集中器(YUTO Nexus)。 | | `smart-dispatcher` | 调度 / 分发。 | diff --git a/backend-modules.json b/backend-modules.json index c9b17802..d7cff982 100644 --- a/backend-modules.json +++ b/backend-modules.json @@ -332,6 +332,38 @@ "pom": "smart-module/smart-transfer/pom.xml", "role": "unpackaged-service", "reason": "当前不在可执行 Jar 白名单;打包与部署能力需单独验收" + }, + { + "artifact_id": "smart-lock", + "pom": "smart-module/smart-lock/pom.xml", + "role": "aggregator" + }, + { + "artifact_id": "smart-lock-api", + "pom": "smart-module/smart-lock/smart-lock-api/pom.xml", + "role": "library" + }, + { + "artifact_id": "smart-lock-biz", + "pom": "smart-module/smart-lock/smart-lock-biz/pom.xml", + "role": "unpackaged-service", + "reason": "门锁开发阶段尚未完成运行装配,禁止纳入可部署服务白名单。" + }, + { + "artifact_id": "smart-bridge-lock", + "pom": "smart-module/smart-bridge-lock/pom.xml", + "role": "aggregator" + }, + { + "artifact_id": "smart-bridge-lock-api", + "pom": "smart-module/smart-bridge-lock/smart-bridge-lock-api/pom.xml", + "role": "library" + }, + { + "artifact_id": "smart-bridge-lock-biz", + "pom": "smart-module/smart-bridge-lock/smart-bridge-lock-biz/pom.xml", + "role": "unpackaged-service", + "reason": "门锁开发阶段尚未完成运行装配,禁止纳入可部署服务白名单。" } ] } diff --git a/contracts/lock/v1/openapi.yaml b/contracts/lock/v1/openapi.yaml new file mode 100644 index 00000000..645165ac --- /dev/null +++ b/contracts/lock/v1/openapi.yaml @@ -0,0 +1,318 @@ +{ + "openapi": "3.1.0", + "info": {"title":"裕慧家园门锁统一公共 API","version":"1.0.0","description":"Java/Oracle 既定范围下采用新平台公共 API 标准。未发布候选契约,运行验收未完成。"}, + "servers": [{"url": "/"}], + "security": [{"bearerAuth": []}], + "paths": { + "/api/v1/locks/assets": {"get":{"operationId":"get__api_v1_locks_assets","summary":"ASSETS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/assets/{deviceId}": {"get":{"operationId":"get__api_v1_locks_assets_deviceId_","summary":"ASSETS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/assets/{deviceId}/config": {"get":{"operationId":"get__api_v1_locks_assets_deviceId_config","summary":"ASSETS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patch__api_v1_locks_assets_deviceId_config","summary":"ASSETS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/devices": {"get":{"operationId":"get__api_v1_locks_devices","summary":"DEVICES 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/gateways": {"get":{"operationId":"get__api_v1_locks_gateways","summary":"GATEWAYS_AND_MODELS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"post__api_v1_locks_gateways","summary":"GATEWAYS_AND_MODELS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/models": {"get":{"operationId":"get__api_v1_locks_models","summary":"GATEWAYS_AND_MODELS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"post__api_v1_locks_models","summary":"GATEWAYS_AND_MODELS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/persons": {"get":{"operationId":"get__api_v1_locks_persons","summary":"PERSONS_AND_CREDENTIALS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/credentials": {"get":{"operationId":"get__api_v1_locks_credentials","summary":"PERSONS_AND_CREDENTIALS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/grants": {"get":{"operationId":"get__api_v1_locks_grants","summary":"GRANTS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"post__api_v1_locks_grants","summary":"GRANTS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/grants/{grantId}": {"get":{"operationId":"get__api_v1_locks_grants_grantId_","summary":"GRANTS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patch__api_v1_locks_grants_grantId_","summary":"GRANTS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/commands": {"get":{"operationId":"get__api_v1_locks_commands","summary":"COMMANDS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/commands/{commandId}": {"get":{"operationId":"get__api_v1_locks_commands_commandId_","summary":"COMMANDS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"commandId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/open-records": {"get":{"operationId":"get__api_v1_locks_open_records","summary":"RECORDS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/password-records": {"get":{"operationId":"get__api_v1_locks_password_records","summary":"RECORDS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/communication-records": {"get":{"operationId":"get__api_v1_locks_communication_records","summary":"RECORDS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/alarms": {"get":{"operationId":"get__api_v1_locks_alarms","summary":"ALARMS_AND_SETTINGS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/alarm-settings": {"get":{"operationId":"get__api_v1_locks_alarm_settings","summary":"ALARMS_AND_SETTINGS 查询","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":true,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patch__api_v1_locks_alarm_settings","summary":"ALARMS_AND_SETTINGS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/assets/{deviceId}/open-requests": {"post":{"operationId":"post__api_v1_locks_assets_deviceId_open_requests","summary":"ASSETS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/devices/{deviceId}": {"patch":{"operationId":"patch__api_v1_locks_devices_deviceId_","summary":"DEVICES 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/devices/{deviceId}/unbinding-requests": {"post":{"operationId":"post__api_v1_locks_devices_deviceId_unbinding_requests","summary":"DEVICES 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/devices/{deviceId}/enablement-requests": {"post":{"operationId":"post__api_v1_locks_devices_deviceId_enablement_requests","summary":"DEVICES 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"deviceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/gateways/{gatewayId}": {"patch":{"operationId":"patch__api_v1_locks_gateways_gatewayId_","summary":"GATEWAYS_AND_MODELS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"gatewayId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/models/{modelId}": {"patch":{"operationId":"patch__api_v1_locks_models_modelId_","summary":"GATEWAYS_AND_MODELS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/credentials/imports": {"post":{"operationId":"post__api_v1_locks_credentials_imports","summary":"PERSONS_AND_CREDENTIALS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/credentials/actions": {"post":{"operationId":"post__api_v1_locks_credentials_actions","summary":"PERSONS_AND_CREDENTIALS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/grants/{grantId}/revocations": {"post":{"operationId":"post__api_v1_locks_grants_grantId_revocations","summary":"GRANTS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/grants/{grantId}/provision-requests": {"post":{"operationId":"post__api_v1_locks_grants_grantId_provision_requests","summary":"GRANTS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"grantId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/commands/{commandId}/actions": {"post":{"operationId":"post__api_v1_locks_commands_commandId_actions","summary":"COMMANDS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"commandId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}},{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/exports": {"post":{"operationId":"post__api_v1_locks_exports","summary":"RECORDS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/locks/alarms/actions": {"post":{"operationId":"post__api_v1_locks_alarms_actions","summary":"ALARMS_AND_SETTINGS 写操作","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"parkId","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"X-Trace-Id","in":"header","required":true,"schema":{"type":"string","minLength":1}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWrite"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Resource"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/status": {"get":{"operationId":"get__api_v1_me_locks_status","summary":"本人门锁状态","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Status"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/password-reveals": {"post":{"operationId":"post__api_v1_me_locks_password_reveals","summary":"短时查看本人密码","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reveal"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevealResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/password-commands": {"post":{"operationId":"post__api_v1_me_locks_password_commands","summary":"修改本人密码","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordCommand"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResult"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/dynamic-code-requests": {"post":{"operationId":"post__api_v1_me_locks_dynamic_code_requests","summary":"请求动态码","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Reveal"}}}},"responses":{"200":{"description":"幂等重试返回已确认终态","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResult"}}}},"202":{"description":"命令已受理,需查询设备确认结果","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/password-reveal-verifications": {"post":{"operationId":"post__api_v1_me_locks_password_reveal_verifications","summary":"签发密码查看验证引用","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/dynamic-code-verifications": {"post":{"operationId":"post__api_v1_me_locks_dynamic_code_verifications","summary":"签发动态码验证引用","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,128}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verification"}}}},"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}, + "/api/v1/me/locks/requests/{requestId}": {"get":{"operationId":"get__api_v1_me_locks_requests_requestId_","summary":"查询本人命令状态","x-runtime-ready":false,"description":"当前提供统一 HTTP 边界;实际身份、持久化、调度和设备接线仍按 specs/011-doorlock-oracle 验收。HTTP 受理不是设备成功。","parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]+$"}}],"responses":{"200":{"description":"成功资源正文,不包裹 code/data","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandResult"}}}},"400":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"稳定错误码;不回显敏感信息","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}} + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer" + } + }, + "schemas": { + "Error": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "minLength": 1 + }, + "message": { + "type": "string", + "minLength": 1 + } + } + }, + "Status": { + "type": "object", + "additionalProperties": false, + "required": [ + "hasCurrentMembership", + "scopeRef", + "authorizationStatus", + "capabilityStatus", + "cutoverFrozen", + "dynamicCodeCapabilityVerified", + "dynamicCodeCompliancePassed" + ], + "properties": { + "scopeRef": { + "type": [ + "string", + "null" + ] + }, + "hasCurrentMembership": { + "type": "boolean" + }, + "authorizationStatus": { + "type": [ + "string", + "null" + ], + "enum": [ + "ACTIVE", + "PENDING_PROVISION", + "PENDING_REVOKE", + "REVOKED", + "RECONCILIATION_REQUIRED", + null + ] + }, + "capabilityStatus": { + "enum": [ + "VERIFIED", + "UNVERIFIED" + ] + }, + "cutoverFrozen": { + "type": "boolean" + }, + "dynamicCodeCapabilityVerified": { + "type": "boolean" + }, + "dynamicCodeCompliancePassed": { + "type": "boolean" + }, + "expectedVersion": { + "type": [ + "string", + "null" + ], + "pattern": "^[A-Za-z0-9_-]{1,128}$", + "description": "领域签发的并发版本;缺失时只读。" + } + } + }, + "PasswordCommand": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "password", + "expectedVersion" + ], + "properties": { + "requestId": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "pattern": "^[0-9]{6}$", + "writeOnly": true + }, + "expectedVersion": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{1,128}$" + } + } + }, + "Reveal": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "verificationRef" + ], + "properties": { + "requestId": { + "type": "string", + "minLength": 1 + }, + "verificationRef": { + "type": "string", + "minLength": 1, + "writeOnly": true + } + } + }, + "Verification": { + "type": "object", + "additionalProperties": false, + "required": [ + "requestId", + "faceEvidence" + ], + "properties": { + "requestId": { + "type": "string", + "minLength": 1 + }, + "faceEvidence": { + "type": "string", + "minLength": 1, + "maxLength": 6291456, + "writeOnly": true + } + } + }, + "VerificationResult": { + "type": "object", + "additionalProperties": false, + "required": [ + "verificationRef" + ], + "properties": { + "verificationRef": { + "type": "string", + "minLength": 1 + } + } + }, + "RevealResult": { + "type": "object", + "additionalProperties": false, + "required": [ + "revealId", + "expiresAt", + "password" + ], + "properties": { + "revealId": { + "type": "string", + "minLength": 1 + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "password": { + "type": "string", + "minLength": 1, + "readOnly": true + } + } + }, + "AdminWrite": { + "type": "object", + "additionalProperties": false, + "required": [ + "parkId", + "requestId", + "reason" + ], + "properties": { + "parkId": { + "type": "integer", + "minimum": 1 + }, + "requestId": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + }, + "expectedVersion": { + "type": "integer", + "minimum": 0 + }, + "payload": { + "type": "object", + "additionalProperties": true + } + } + }, + "Resource": { + "type": "object", + "description": "管理领域 DTO 尚在接线阶段,细化响应与运行验证是后续门禁,不得据此发布客户端生成模型。", + "additionalProperties": true + }, + "CommandResult": { + "type": "object", + "additionalProperties": false, + "required": [ + "httpStatus", + "traceId", + "requestId", + "commandId", + "commandStatus", + "transportAccepted", + "deviceConfirmed", + "terminal", + "completed" + ], + "properties": { + "httpStatus": { + "type": "integer", + "enum": [ + 200, + 202 + ] + }, + "traceId": { + "type": "string" + }, + "requestId": { + "type": "string" + }, + "commandId": { + "type": "string" + }, + "commandStatus": { + "type": "string" + }, + "transportAccepted": { + "type": "boolean" + }, + "deviceConfirmed": { + "type": "boolean" + }, + "terminal": { + "type": "boolean" + }, + "completed": { + "type": "boolean" + } + } + } + } + } +} diff --git a/scripts/test-backend-modules.py b/scripts/test-backend-modules.py index 1d077217..169212e5 100644 --- a/scripts/test-backend-modules.py +++ b/scripts/test-backend-modules.py @@ -28,7 +28,7 @@ def save(self): def test_all_registered_modules(self): self.assertEqual(validate(self.root, self.plan), []) - self.assertEqual(len(self.inventory['modules']), 60) + self.assertEqual(len(self.inventory['modules']), 66) def test_concentrator_revision_drift(self): p = self.root / 'smart-module/smart-bridge-concentrator/pom.xml' diff --git a/smart-h5/docs/superpowers/doorlock/README.md b/smart-h5/docs/superpowers/doorlock/README.md index e9ff5cb8..094aca03 100644 --- a/smart-h5/docs/superpowers/doorlock/README.md +++ b/smart-h5/docs/superpowers/doorlock/README.md @@ -4,7 +4,7 @@ 当前实现以 [011 门锁统一规格](../../../../specs/011-doorlock-oracle/spec.md)、[Web/H5 契约](../../../../specs/011-doorlock-oracle/contracts/web-h5.md) 和 [任务清单](../../../../specs/011-doorlock-oracle/tasks.md) 为准。本目录保留页面历史参考;旧接口目标、凭据处置及切换假设的冲突按 [设计修订记录](../../../../specs/011-doorlock-oracle/design-revisions.md) 修正,不再单独驱动实现。 -H5 通过平台业务入口取得本人门锁能力,由服务端登录态确定身份;不再信任客户端传入的工号决定凭据目标。HTTP 受理与设备确认分开展示,密码不持久保留在浏览器。本轮仅更新设计,没有改动 H5 代码。 +H5 的目标是通过平台业务入口取得本人门锁能力,由服务端登录态确定身份;不信任客户端传入的工号决定凭据目标。HTTP 受理与设备确认分开展示,密码不持久保留在浏览器。2026-09-07 已完成 H5 API 和三处目标页面的离线接线:旧 `badge` 密码读取/更新/刷新调用及浏览器 AES 解密已从目标页面移除;查看/刷新均通过固定用途的验证签发路径后执行。该状态仍仅是离线合同验证:验证门面未接真实算法、Oracle、网关或设备,生产请求会失败关闭,不能将其描述为生产可用或切换完成;详细要求见 [test-and-acceptance.md](test-and-acceptance.md)。 ## 目标 diff --git a/smart-h5/docs/superpowers/doorlock/api-cutover-contract.md b/smart-h5/docs/superpowers/doorlock/api-cutover-contract.md index a6a469b6..30fec9e6 100644 --- a/smart-h5/docs/superpowers/doorlock/api-cutover-contract.md +++ b/smart-h5/docs/superpowers/doorlock/api-cutover-contract.md @@ -1,8 +1,16 @@ # H5 到 smart-lock 的接口切换契约 -## 当前调用事实 +## 2026-09-08 版本与命令响应 -当前 smart-h5 的 dorm feature 已封装以下“本人”语义调用,定义位于 smart-h5/src/features/dorm/api.ts: +- 状态响应新增 `expectedVersion`;缺失、空值或非有界字符串时禁用修改密码,不能从 scopeRef 或客户端生成版本。 +- `POST /api/v1/me/locks/password-commands` 正文必须带 requestId/password/expectedVersion;幂等键只发标准请求头。 +- HTTP 409 清空并关闭修改表单,重新读取状态,不自动重试密码下发。当前其他网络故障后的可靠原请求重试/查询仍待后续闭环。 +- 密码修改、动态码请求和请求查询返回 CommandResult,不能把 HTTP 200/202 或 transportAccepted 解释为设备完成。动态码请求不返回明文密码。 +- 本增量仅通过本地 Mock/单测验证;版本签发和事务条件更新待领域适配器接线,未获真实 Oracle 或设备验收。 + +## 旧调用与当前切换实现 + +下列旧调用仍保留在 dorm API 适配层,供尚未迁移的兼容范围识别;`/dorm/lock`、`/dorm/get-code` 与入住详情已不再调用它们: | 语义 | 当前路径表达 | 前端边界 | |---|---|---| @@ -10,7 +18,11 @@ | 修改本人门锁密码 | `POST /dormitory/staff/update/lock/pwd`(body:`badge`、`newPwd`) | 仅提交受控密码输入;结果需区分已受理与设备确认。 | | 刷新本人动态密码 | `POST /dormitory/staff/update/pwd`(body:`badge`、`facePic`) | 人脸或其他服务端校验结果必须由后端裁定。 | -以上仅确认客户端调用事实;携带 `badge` 不能证明后端已将其与 token 身份绑定,服务端鉴权需单独验证。这些当前路径是现有 Smart 平台接口表达,不是未来 smart-lock 的最终路径。切换前不得仅改前端 URL;必须先冻结版本化 DTO、错误码、鉴权、幂等、状态和回退兼容策略。 +以上仅确认旧客户端调用事实;携带 `badge` 不能证明后端已将其与 token 身份绑定。它们不是新门锁合同,切换前不得重新接回页面。 + +当前一期页面使用的本人路径为 `GET /api/v1/me/locks/status`、`POST /password-commands`、`POST /password-reveal-verifications`、`POST /password-reveals`、`POST /dynamic-code-verifications` 与 `POST /dynamic-code-requests`。验证签发和执行均从当前登录态解析本人范围;两个 `verifications` 路径分别固定为“查看密码”和“刷新动态码”,不接收客户端 `purpose`、`badge`、人员、租户、房间、设备或客户端人脸成功结论。 + +截至 2026-09-07,此实现是离线合同与失败关闭接线:验证端口尚未装配真实算法/人脸参考资料适配器,也未接 Oracle、网关或门锁设备;生产请求会拒绝,不能宣称可用或用于切换。 ## 新接口必须保证的语义 diff --git a/smart-h5/docs/superpowers/doorlock/page-and-state-contract.md b/smart-h5/docs/superpowers/doorlock/page-and-state-contract.md index 39e655d9..a8d79bf5 100644 --- a/smart-h5/docs/superpowers/doorlock/page-and-state-contract.md +++ b/smart-h5/docs/superpowers/doorlock/page-and-state-contract.md @@ -5,8 +5,8 @@ | 路由 | 当前职责 | 关键实现位置 | |---|---|---| | /dorm | 宿舍总入口,跳转门锁服务。 | smart-h5/src/app/dorm/page.tsx | -| /dorm/lock | 读取本人动态密码、展示/修改、处理未入住及结果提示。 | smart-h5/src/app/dorm/lock/page.tsx | -| /dorm/get-code | 摄像头、人脸校验、动态码刷新和返回。 | smart-h5/src/app/dorm/get-code/page.tsx | +| /dorm/lock | 读取本人门锁状态;不自动读取/解密密码,可提交修改或进入受控查看/刷新流程。 | smart-h5/src/app/dorm/lock/page.tsx | +| /dorm/get-code | 摄像头采集、服务端本人核验、动态码刷新或短时查看和返回。 | smart-h5/src/app/dorm/get-code/page.tsx | | 宿舍人脸组件 | 扫描、校验、成功、失败状态机。 | smart-h5/src/features/dorm/lock-face-camera.tsx | ## 本人身份规则 @@ -18,22 +18,21 @@ | 状态 | 触发条件 | 页面反馈 | 可用动作 | |---|---|---|---| | 加载中 | 初次查询、密码修改提交、刷新动态码。 | 明确 loading,不能复用旧值冒充实时。 | 等待或安全取消。 | -| 有效 | 当前人有有效住宿和可展示的门锁结果。 | 展示最小必要结果、更新时间和使用说明。 | 受控修改本人密码、进入刷新流程。 | +| 有效 | 当前人有有效住宿和可展示的门锁状态。 | 不自动显示密码;说明需二次核验。 | 受控修改本人密码、进入查看或刷新流程。 | | 未入住/无门锁 | 服务端无有效住宿或门锁权限。 | 可理解说明,不显示他人或设备细节。 | 返回宿舍入口、查看帮助。 | | 校验中 | 人脸流程正在扫描或服务端核验。 | 明确阶段文字与隐私提示。 | 取消或重试,遵循次数限制。 | -| 刷新成功 | 服务端返回新动态码/结果。 | 短时展示结果,页面离开或失效后清除。 | 返回门锁页。 | +| 刷新/查看成功 | 服务端返回新动态码或受控查看结果。 | 密码只在当前短时弹窗呈现,不能写 Query、store、URL 或持久化浏览器存储。 | 返回门锁页。 | | 刷新失败 | 人脸失败、限频、服务异常、设备不支持或系统冻结。 | 不同原因给出不同的安全文案,不能统一伪装成“网络异常”。 | 按原因重试、查看帮助或联系宿管。 | | 已受理待确认 | 本人密码或门锁动作已提交但设备尚未确认。 | 显示“已受理/确认中”,不显示已生效。 | 刷新状态、查看帮助。 | ## 修改本人密码 -当前页面有“新密码为 6 位数字、不能与原密码相同”的前端体验约束。新 smart-lock 合同还必须负责身份、频率、设备能力、有效住宿、幂等和真实设备确认。H5 页面不记录密码历史、不打印到日志、不将密码放入 URL 或持久化存储。 +当前页面有“新密码为 6 位数字”的前端体验约束;由于页面不再读取旧密码,是否允许与当前密码相同必须由新 smart-lock 合同裁定。合同还必须负责身份、频率、设备能力、有效住宿、幂等和真实设备确认。H5 页面不记录密码历史、不打印到日志、不将密码放入 URL 或持久化存储。 ## 人脸刷新流程 -人脸组件的前端状态机包含 initial、scanning、checking、success、failure。每个状态应有明确文字和可恢复路径。相机权限拒绝、摄像头不可用、认证超时、连续失败、系统冻结均需独立可理解提示;前端动画或倒计时不能作为身份核验成功的依据。 +人脸组件的前端状态机包含 initial、scanning、checking、success、failure。`success` 在当前组件仅表示材料采集完成,不是本人核验通过;点击动作后才由服务端以固定用途、当前身份和当前住宿签发一次性 `verificationRef`。相机权限拒绝、摄像头不可用、认证超时、连续失败、系统冻结均需独立可理解提示;前端动画或倒计时不能作为身份核验成功的依据。 ## 缓存与失效 本人门锁结果与入住详情共享的查询键必须在修改密码、刷新动态码、调宿、退宿、登录身份变化、登出、授权撤销和运行模式变化后失效或重新读取。页面保留最后成功时间时,应明确标为历史数据,不能冒充实时状态。 - diff --git a/smart-h5/docs/superpowers/doorlock/test-and-acceptance.md b/smart-h5/docs/superpowers/doorlock/test-and-acceptance.md index ea06c374..3d960f5e 100644 --- a/smart-h5/docs/superpowers/doorlock/test-and-acceptance.md +++ b/smart-h5/docs/superpowers/doorlock/test-and-acceptance.md @@ -17,8 +17,38 @@ | 退出与失效 | 登出、身份切换、调宿、撤权、到期、页面卸载后清除短时结果。 | | 可用性 | 弱网、刷新、重复进入、屏幕阅读和键盘替代操作(可用时)有可解释结果。 | +## 2026-09-07 T079 / T083 纯状态与短时内存验证 + +- 实现文件:`src/features/dorm/lock-state.ts` 与 `lock-state.test.ts`。T079 测试开发项已完成;T083 仅完成可复用状态/内存子范围,当前旧 API 和页面未被替换。 +- 从 `smart-h5/` 执行 `pnpm exec vitest run src/features/dorm/lock-state.test.ts`:**1 file / 38 tests passed**;两文件 ESLint、Prettier check 与 `pnpm exec tsc --noEmit --pretty false --incremental false` 均退出 0。未运行本批 H5 build/E2E,未连接真实服务。 +- 红灯覆盖模块缺失、34 项未实现行为,以及完成前新增的 4 项缺陷回归;最终修复了缺失/重复 commandId 误算成功、不同实例同代次票据混用、通用 VERIFIED 错误打开动态码三类问题。独立复审确认规格/质量通过;额外只读复现确认计数矛盾进入对账、显式清空作用域淘汰在途结果。 +- 展示区分确认中、撤权中、已撤销、对账、无住宿、冻结及能力未知;HTTP/传输受理不等于设备确认。动态码还要求独立能力和合规证据。 +- 密码仅存组件实例闭包,普通 getState 不包含秘密;固定用途、到期计时、身份/住宿/冻结变化、修改开始、登出和销毁均有失效保护,不使用 QueryCache、localStorage 或 sessionStorage 存门锁秘密。 + +## 2026-09-07 T081 / T082 / T084 离线接线证据 + +- 后端本人入口增加冻结的 `/platform/lock/v1/me` 合同、当前身份/住宿重校验、未知字段拒绝、查看 no-store,以及按固定路径签发查看/动态码的一次性验证引用。未装配 `MyLockDomainFacade` 或 `MyLockVerificationFacade` 时明确失败关闭;离线 Maven 回归为 **2 files / 14 tests passed**。 +- H5 `api.ts`、`/dorm/lock`、`/dorm/get-code` 与 `/check-in/detail` 已迁出旧 `badge` 密码 API 和 AES 解密。动态码和查看页均先签发验证引用再执行;采集材料和预览在请求结束时清理,查看结果只交给当前短时弹窗。H5 `pnpm check` 与门锁相关 **3 files / 42 tests passed**。 +- 本地 Playwright 已覆盖新本人状态/修改、未入住、验证签发后刷新、状态与修改失败、入住详情不读取历史密码、网络超时、人脸材料质量失败和浏览器持久化存储不含测试密码:`e2e/help-dorm-lock.spec.ts`、`e2e/check-in.spec.ts` 与 `e2e/dorm-lock.spec.ts` 共 **18 tests passed**。全部 API、摄像头和人脸材料均为 Mock;E2E 不等于真机或真实服务验收。 +- 使用临时合成 AES 值和回环代理地址的 H5 `pnpm build` 已通过;它只证明编译与静态路由生成,未使用生产密钥或生产代理。 +- 未运行真实服务、算法、人脸资料、Oracle、网关、门锁、微信真机或数据迁移。上述测试不能证明人脸核验、设备下发、撤权或切换可用。 + +后续 T083/T084 页面状态接线及真实适配必须遵循下列顺序: + +1. 每个页面实例单独创建控制器,保留原票据对象;不得序列化、重建或跨实例传递票据。API 仍由服务端授权,客户端 scope 只负责失效与展示。 +2. 对同 membership 的授权撤销、动态码刷新、非冻结模式变化、授权版本变化,先调用 `setScope(null)` 清除秘密和在途票据,再刷新服务端事实;只有新事实允许时才设置有效 scope。不能只重复传入相同 setScope / setFrozen(false),其同值调用是 no-op。 +3. 组件卸载调用 destroy;登出调用 logout;冻结先 setFrozen(true)。迟到响应必须持原票据交给控制器核验,不能直接 setState 或恢复旧 Query 数据。 +4. 普通状态/请求查询禁止读取或缓存密码。reveal 的 no-store HTTP、一次性 verificationRef、真实身份与住宿事实、页面 DOM 清理、旧 Query 清除及微信场景必须随真实适配再做集成与真机验证。 + ## 验收限制 +### T083 主页门禁与服务端状态 DTO 增量 + +- `/dorm/lock` 已消费 `resolveLockDisplayState` 与组件专属 `createLockStateController`:冻结、撤权、能力未知或缺少 `scopeRef` 禁用敏感按钮;重新读取状态先清空作用域,修改请求通过原票据处理响应,卸载销毁控制器。 +- 服务端 `MyLockDomainFacade.status` 改为 `MyLockStatus` 白名单 DTO。有住宿缺少引用时拒绝,无住宿清除旧引用与授权,缺失能力默认为 UNVERIFIED。作用域引用实际签发/持久化适配仍待实现;不能由前端构造或当授权凭证。 +- 实际回归:后端 MyLockStatusTest / MyLockControllerTest / MyLockApiContractTest 共 17 项通过;前端 43 项单测与类型/静态检查通过;三个相关 Playwright 文件同轮 21 项通过。冻结按钮测试先复现 enabled 错误再通过。 +- T083/T084 仍未完成:`/dorm/get-code` 尚需接入作用域票据、服务端 revealId/expiresAt、到期 DOM 清理、身份失效及迟到响应淘汰;主页完整认证变更订阅、旧 Query 淘汰、幂等重试与请求结果轮询也待补齐。以上均可继续在开发环境实现,真实环境验证另列。 + - H5 不以旧 /key/myKey 或 /key/record 作为一期必测页面,除非 OPEN-H5-001 被明确纳入并补充需求与安全评审。 - 任何本人远程开门能力都需要协议、合规和业务授权 Gate;不能因为旧 bundle 存在按钮就上线。 - 动态码、密码和人脸相关测试的证据不得包含真实生物数据、明文密码、token 或完整个人信息。 @@ -26,4 +56,3 @@ ## 发布前证据 每次涉及 smart-h5/src/app/dorm、smart-h5/src/features/dorm 或认证缓存的改动,都应关联单测、E2E/真机结果、接口契约版本、敏感字段扫描和人工验收截图。若 smart-lock 尚未提供新接口,测试报告必须明确标记为 mock/集成环境结果,不能称为生产可用。 - diff --git a/smart-h5/e2e/check-in.spec.ts b/smart-h5/e2e/check-in.spec.ts index a24e966f..91de7db5 100644 --- a/smart-h5/e2e/check-in.spec.ts +++ b/smart-h5/e2e/check-in.spec.ts @@ -1,5 +1,4 @@ import { expect, test, type Page } from '@playwright/test' -import { injectTestKey, legacyCipherHex } from './helpers' async function seedLogin(page: Page) { await page.addInitScript(() => { @@ -212,10 +211,11 @@ test('宿舍申请:切换楼栋清空房间类型与已选房间', async ({ pa await expect(page.getByRole('button', { name: '请选择房间', exact: true })).toBeVisible() }) -test('宿舍申请详情:分配记录 + 动态码解密展示', async ({ page }) => { +test('宿舍申请详情:分配记录与门锁特征状态,不读取或展示历史动态码', async ({ page }) => { await seedLogin(page) await mockBaseInfo(page) - await injectTestKey(page) + const requests: string[] = [] + page.on('request', (request) => requests.push(new URL(request.url()).pathname)) await page.route('**/platform/dormitory/staff/roomList/YT20180326', (route) => route.fulfill({ json: { @@ -236,18 +236,15 @@ test('宿舍申请详情:分配记录 + 动态码解密展示', async ({ page }, }), ) - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: legacyCipherHex('246810') } }), - ) - await page.goto('/check-in/detail') await expect(page.getByText('新工厂宿舍楼')).toBeVisible() await expect(page.getByText('302房间')).toBeVisible() await expect(page.getByText('4号床')).toBeVisible() await expect(page.getByText('指纹已录入')).toBeVisible() await expect(page.getByText('已录入', { exact: true })).toBeVisible() - // 密文经 AES 解出明文动态码 - await expect(page.getByTestId('checkin-lock-code')).toHaveText('246810') + await expect(page.getByTestId('checkin-lock-code')).toHaveCount(0) + await expect(page.getByText('246810')).toHaveCount(0) + expect(requests.some((path) => path.includes('/dormitory/staff/get/pwd'))).toBe(false) }) test('死链回归:home 宫格「宿舍申请」入口落到真实页', async ({ page }) => { diff --git a/smart-h5/e2e/dorm-lock.spec.ts b/smart-h5/e2e/dorm-lock.spec.ts new file mode 100644 index 00000000..88070451 --- /dev/null +++ b/smart-h5/e2e/dorm-lock.spec.ts @@ -0,0 +1,174 @@ +import { expect, test, type Page } from '@playwright/test' + +test('未提供并发版本时禁止修改密码', async ({ page }) => { + await seedLogin(page) + await page.route('**/api/v1/me/locks/status', route => route.fulfill({ json: { + scopeRef: 'scope-readonly', hasCurrentMembership: true, authorizationStatus: 'ACTIVE', + capabilityStatus: 'VERIFIED', cutoverFrozen: false, + } })) + await page.goto('/dorm/lock') + await expect(page.getByRole('button', { name: '修改动态码', exact: true })).toBeDisabled() +}) + +test('版本冲突刷新状态但不自动重新下发密码', async ({ page }) => { + await seedLogin(page) + let writes = 0 + let reads = 0 + await page.route('**/api/v1/me/locks/status', route => { + reads += 1 + return route.fulfill({ json: { + scopeRef: 'scope-conflict', expectedVersion: 'version-' + reads, + hasCurrentMembership: true, authorizationStatus: 'ACTIVE', + capabilityStatus: 'VERIFIED', cutoverFrozen: false, + } }) + }) + await page.route('**/api/v1/me/locks/password-commands', route => { + writes += 1 + return route.fulfill({ status: 409, json: { code: 'VERSION_CONFLICT' } }) + }) + await page.goto('/dorm/lock') + await page.getByRole('button', { name: '修改动态码', exact: true }).click() + await page.getByPlaceholder('请输入6位数字动态码').fill('654321') + await page.getByRole('button', { name: '确定', exact: true }).click() + await expect(page.getByText('请核对最新门锁状态后重新操作。')).toBeVisible() + await expect.poll(() => reads).toBeGreaterThan(1) + await expect(page.getByPlaceholder('请输入6位数字动态码')).toBeHidden() + await expect(page.getByPlaceholder('请输入6位数字动态码')).toHaveValue('') + expect(writes).toBe(1) +}) + +for (const restricted of [ + { label: '已撤权', scopeRef: 'scope-revoked', authorizationStatus: 'REVOKED' }, + { label: '缺少作用域', scopeRef: null, authorizationStatus: 'ACTIVE' }, +]) { + test(`${restricted.label}禁止敏感操作`, async ({ page }) => { + await seedLogin(page) + await page.route('**/api/v1/me/locks/status', route => route.fulfill({ json: { ...restricted, hasCurrentMembership: true, capabilityStatus: 'VERIFIED', + cutoverFrozen: false, dynamicCodeCapabilityVerified: true, dynamicCodeCompliancePassed: true } })) + await page.goto('/dorm/lock') + for (const name of ['修改动态码', '查看动态码', '刷新动态码(人脸识别)']) { + await expect(page.getByRole('button', { name, exact: true })).toBeDisabled() + } + }) +} + +test('冻结状态禁止查看修改和刷新', async ({ page }) => { + await seedLogin(page) + await page.route('**/api/v1/me/locks/status', route => route.fulfill({ json: { scopeRef: 'scope-1', hasCurrentMembership: true, + authorizationStatus: 'ACTIVE', capabilityStatus: 'VERIFIED', cutoverFrozen: true } })) + await page.goto('/dorm/lock') + await expect(page.getByRole('button', { name: '修改动态码', exact: true })).toBeDisabled() + await expect(page.getByRole('button', { name: '查看动态码', exact: true })).toBeDisabled() + await expect(page.getByRole('button', { name: '刷新动态码(人脸识别)' })).toBeDisabled() + await expect(page.getByText('门锁模式已冻结')).toBeVisible() +}) + +/** E2E 仅伪造登录态,不接触真实认证、数据库、网关或门锁。 */ +async function seedLogin(page: Page) { + await page.addInitScript(() => { + localStorage.setItem( + 'xc-access_token', + JSON.stringify({ dataType: 'string', content: 'tok-doorlock-e2e', datetime: Date.now() }), + ) + }) +} + +/** 用浏览器内合成前置摄像头覆盖真实媒体设备。 */ +async function mockFrontCamera(page: Page) { + await page.addInitScript(() => { + Object.defineProperty(navigator, 'mediaDevices', { + configurable: true, + value: { getUserMedia: async () => new MediaStream() }, + }) + Object.defineProperty(HTMLVideoElement.prototype, 'play', { + configurable: true, + value: async function () { + Object.defineProperty(this, 'videoWidth', { configurable: true, get: () => 320 }) + Object.defineProperty(this, 'videoHeight', { configurable: true, get: () => 240 }) + }, + }) + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { + configurable: true, + value: () => ({ drawImage() {} }), + }) + Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', { + configurable: true, + value: () => 'data:image/jpeg;base64,camera-raw-base64', + }) + }) +} + +test('本人门锁主页仅调用新状态和修改契约,且不在页面展示密码', async ({ page }) => { + await seedLogin(page) + const requests: string[] = [] + page.on('request', (request) => requests.push(new URL(request.url()).pathname)) + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ json: { scopeRef: 'scope-e2e', expectedVersion: 'version-e2e', hasCurrentMembership: true, + authorizationStatus: 'ACTIVE', capabilityStatus: 'VERIFIED', cutoverFrozen: false, + dynamicCodeCapabilityVerified: true, dynamicCodeCompliancePassed: true } }), + ) + let changeBody: Record | undefined + await page.route('**/api/v1/me/locks/password-commands', async (route) => { + changeBody = route.request().postDataJSON() as Record + await route.fulfill({ status: 202, json: { status: 'ACCEPTED' } }) + }) + + await page.goto('/dorm/lock') + await expect(page.getByTestId('lock-code')).toHaveText('******') + await expect(page.getByText('为保护宿舍安全,请完成人脸核验后短时查看当前动态码。')).toBeVisible() + await page.getByRole('button', { name: '修改动态码' }).click() + await page.getByPlaceholder('请输入6位数字动态码').fill('654321') + await page.getByRole('button', { name: '确定' }).click() + + await expect.poll(() => changeBody).toBeDefined() + expect(changeBody).toMatchObject({ password: '654321', expectedVersion: 'version-e2e' }) + expect(changeBody).not.toHaveProperty('badge') + await expect + .poll(() => + page.evaluate(() => { + const values = [...Object.values(localStorage), ...Object.values(sessionStorage)] + return values.some((value) => value.includes('654321')) + }), + ) + .toBe(false) + expect(requests.some((path) => path.includes('/dormitory/staff/get/pwd'))).toBe(false) + expect(requests.some((path) => path.includes('/dormitory/staff/update/lock/pwd'))).toBe(false) +}) + +test('本人门锁状态网络超时只展示可重试错误,不伪造可用动态码', async ({ page }) => { + await seedLogin(page) + await page.route('**/api/v1/me/locks/status', (route) => route.abort('timedout')) + + await page.goto('/dorm/lock') + await expect(page.getByText('门锁状态获取失败')).toBeVisible() + await expect(page.getByRole('button', { name: '重试' })).toBeVisible() + await expect(page.getByTestId('lock-code')).toHaveCount(0) +}) + +test('人脸材料质量失败时不签发验证引用或请求动态码', async ({ page }) => { + await seedLogin(page) + await mockFrontCamera(page) + await page.route('**/app/employee/baseinfo', (route) => + route.fulfill({ json: { code: 0, data: { employeeName: '测试员工', employeeBadge: 'TEST-1' } } }), + ) + await page.route('**/algorithm/out/face/cut', (route) => + route.fulfill({ json: { code: 1, message: '未检测到人脸' } }), + ) + let issued = false + let requested = false + await page.route('**/api/v1/me/locks/dynamic-code-verifications', (route) => { + issued = true + return route.fulfill({ json: { verificationRef: 'must-not-exist' } }) + }) + await page.route('**/api/v1/me/locks/dynamic-code-requests', (route) => { + requested = true + return route.fulfill({ status: 202, json: { commandStatus: 'QUEUED', deviceConfirmed: false } }) + }) + + await page.goto('/dorm/get-code') + await page.getByRole('button', { name: '打开摄像头' }).click() + await page.getByRole('button', { name: '拍照核验' }).click() + await expect(page.getByText('未检测到人脸')).toBeVisible() + expect(issued).toBe(false) + expect(requested).toBe(false) +}) diff --git a/smart-h5/e2e/help-dorm-lock.spec.ts b/smart-h5/e2e/help-dorm-lock.spec.ts index 0d335268..f52bbd4f 100644 --- a/smart-h5/e2e/help-dorm-lock.spec.ts +++ b/smart-h5/e2e/help-dorm-lock.spec.ts @@ -149,26 +149,22 @@ test('水电明细:空态', async ({ page }) => { await expect(page.getByText('暂无数据')).toBeVisible() }) -// ===== 门锁(注入测试 key,用旧算法现场生成真密文) ===== -import { injectTestKey, legacyCipherHex } from './helpers' - -test('门锁:密文解密展示 + 修改三连', async ({ page }) => { +// ===== 门锁(新本人契约;全部为本地 Mock) ===== +test('门锁:状态遮罩展示 + 修改六位校验和无 badge 请求', async ({ page }) => { await seedLogin(page) - await injectTestKey(page) - await mockBaseInfo(page) - let pwdCipher = legacyCipherHex('123456') - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: pwdCipher } }), + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ json: { scopeRef: 'scope-e2e', expectedVersion: 'version-e2e', hasCurrentMembership: true, + authorizationStatus: 'ACTIVE', capabilityStatus: 'VERIFIED', cutoverFrozen: false, + dynamicCodeCapabilityVerified: true, dynamicCodeCompliancePassed: true } }), ) let updateBody: Record | undefined - await page.route('**/platform/dormitory/staff/update/lock/pwd', async (route) => { + await page.route('**/api/v1/me/locks/password-commands', async (route) => { updateBody = route.request().postDataJSON() as Record - pwdCipher = legacyCipherHex('654321') - await route.fulfill({ json: { code: 0 } }) + await route.fulfill({ status: 202, json: { commandStatus: 'QUEUED', deviceConfirmed: false } }) }) await page.goto('/dorm/lock') - await expect(page.getByTestId('lock-code')).toHaveText('123456') + await expect(page.getByTestId('lock-code')).toHaveText('******') await page.getByRole('button', { name: '修改动态码' }).click() const input = page.getByPlaceholder('请输入6位数字动态码') @@ -176,22 +172,17 @@ test('门锁:密文解密展示 + 修改三连', async ({ page }) => { await page.getByRole('button', { name: '确定' }).click() await expect(page.getByText('请输入6位数字动态码')).toBeVisible() - await input.fill('123456') - await page.getByRole('button', { name: '确定' }).click() - await expect(page.getByText('请输入跟当前动态码不一样的新的动态码')).toBeVisible() - await input.fill('654321') await page.getByRole('button', { name: '确定' }).click() - await expect(page.getByTestId('lock-code')).toHaveText('654321') - expect(updateBody).toEqual({ badge: 'YT20180326', newPwd: '654321' }) + await expect.poll(() => updateBody).toBeDefined() + expect(updateBody).toMatchObject({ password: '654321' }) + expect(updateBody).not.toHaveProperty('badge') }) test('门锁:未入住 alert 回跳 /dorm', async ({ page }) => { await seedLogin(page) - await injectTestKey(page) - await mockBaseInfo(page) - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: '' } }), + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ json: { hasCurrentMembership: false } }), ) await page.goto('/dorm/lock') @@ -200,53 +191,46 @@ test('门锁:未入住 alert 回跳 /dorm', async ({ page }) => { await page.waitForURL('**/dorm') }) -test('刷新动态码:人脸比对 → 生成 → 回门锁页', async ({ page }) => { +test('刷新动态码:采集材料 → 服务端签发 → 刷新 → 回门锁页', async ({ page }) => { await seedLogin(page) - await injectTestKey(page) await mockBaseInfo(page) await mockFrontCamera(page) let cutBody: Record | undefined - let checkFaceBody: Record | undefined await page.route('**/algorithm/out/face/cut', async (route) => { cutBody = route.request().postDataJSON() as Record await route.fulfill({ json: { code: 0, message: 'success', data: 'cut-base64' } }) }) - // Real checkFace returns data.photoId and no resultData.base64; lock refresh must reuse uploaded base64. - await page.route('**/app/wechat/visit/checkFace', async (route) => { - checkFaceBody = route.request().postDataJSON() as Record - await route.fulfill({ - json: { code: 0, message: 'success', data: { photoId: 'p-lock' } }, - }) - }) - // 哨兵 value 不能被当成 photoId 去回查网关图片(坏图回归防护)。 - let brokenImg = false - await page.route('**/platform/image/view/**', (route) => { - brokenImg = true - return route.fulfill({ status: 404, body: '' }) + let verificationBody: Record | undefined + await page.route('**/api/v1/me/locks/dynamic-code-verifications', async (route) => { + verificationBody = route.request().postDataJSON() as Record + await route.fulfill({ json: { verificationRef: 'verification-e2e' } }) }) let refreshBody: Record | undefined - await page.route('**/platform/dormitory/staff/update/pwd', async (route) => { + await page.route('**/api/v1/me/locks/dynamic-code-requests', async (route) => { refreshBody = route.request().postDataJSON() as Record - await route.fulfill({ json: { code: 0 } }) + await route.fulfill({ status: 202, json: { commandStatus: 'QUEUED', deviceConfirmed: false } }) }) - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: legacyCipherHex('888888') } }), + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ json: { scopeRef: 'scope-e2e', expectedVersion: 'version-e2e', hasCurrentMembership: true, + authorizationStatus: 'ACTIVE', capabilityStatus: 'VERIFIED', cutoverFrozen: false, + dynamicCodeCapabilityVerified: true, dynamicCodeCompliancePassed: true } }), ) await page.goto('/dorm/get-code') await expect(page.locator('input[type="file"]')).toHaveCount(0) await page.getByRole('button', { name: '打开摄像头' }).click() - await page.getByRole('button', { name: '拍照识别' }).click() - await expect(page.getByText('人脸对比成功')).toBeVisible() + await page.getByRole('button', { name: '拍照核验' }).click() + await expect(page.getByText('人脸采集完成')).toBeVisible() expect(cutBody).toEqual({ imageData: 'camera-raw-base64' }) - expect(checkFaceBody).toEqual({ visitorPhoto: 'cut-base64' }) - expect(brokenImg).toBe(false) await page.getByRole('button', { name: '生成动态码' }).click() - await expect(page.getByText('刷新动态码成功!')).toBeVisible() - expect(refreshBody).toEqual({ badge: 'YT20180326', facePic: 'cut-base64' }) + await expect(page.getByText('刷新请求已受理,门锁生效结果待确认。')).toBeVisible() + expect(verificationBody).toMatchObject({ faceEvidence: 'cut-base64' }) + expect(verificationBody).not.toHaveProperty('badge') + expect(refreshBody).toMatchObject({ verificationRef: 'verification-e2e' }) + expect(refreshBody).not.toHaveProperty('badge') await page.getByRole('button', { name: '确定' }).click() await page.waitForURL('**/dorm/lock') - await expect(page.getByTestId('lock-code')).toHaveText('888888') + await expect(page.getByTestId('lock-code')).toHaveText('******') }) test('帮助中心:上拉加载第二页(追加不重复)', async ({ page }) => { @@ -269,40 +253,27 @@ test('帮助中心:上拉加载第二页(追加不重复)', async ({ page await expect(page.locator('button', { hasText: /^问题/ })).toHaveCount(11) }) -test('门锁:修改失败 alert、解密失败 toast、接口业务失败重试块', async ({ page }) => { +test('门锁:状态与修改接口失败都给出受控错误态', async ({ page }) => { await seedLogin(page) - await injectTestKey(page) - await mockBaseInfo(page) - // 1) 解密失败(非法 hex)→ ****** + toast - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: 'not-valid-hex-!!' } }), - ) - await page.goto('/dorm/lock') - await expect(page.getByTestId('lock-code')).toHaveText('******') - await expect(page.getByText('动态码解析失败')).toBeVisible() - // 解密失败时修改按钮禁用 - await expect(page.getByRole('button', { name: '修改动态码' })).toBeDisabled() - - // 2) 接口业务失败 → 重试块(不显示伪装码) - await page.unroute('**/platform/dormitory/staff/get/pwd*') - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 1, msg: '动态码服务繁忙' } }), + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ status: 503, json: { code: 'LOCK_UNAVAILABLE', message: '门锁服务暂不可用' } }), ) await page.goto('/dorm/lock') - await expect(page.getByText('动态码服务繁忙')).toBeVisible() + await expect(page.getByText('门锁状态获取失败')).toBeVisible() await expect(page.getByRole('button', { name: '重试' })).toBeVisible() - // 3) 修改失败 alert - await page.unroute('**/platform/dormitory/staff/get/pwd*') - await page.route('**/platform/dormitory/staff/get/pwd*', (route) => - route.fulfill({ json: { code: 0, data: legacyCipherHex('123456') } }), + await page.unroute('**/api/v1/me/locks/status') + await page.route('**/api/v1/me/locks/status', (route) => + route.fulfill({ json: { scopeRef: 'scope-e2e', expectedVersion: 'version-e2e', hasCurrentMembership: true, + authorizationStatus: 'ACTIVE', capabilityStatus: 'VERIFIED', cutoverFrozen: false, + dynamicCodeCapabilityVerified: true, dynamicCodeCompliancePassed: true } }), ) - await page.route('**/platform/dormitory/staff/update/lock/pwd', (route) => - route.fulfill({ json: { code: 1, msg: '动态码修改过于频繁' } }), + await page.route('**/api/v1/me/locks/password-commands', (route) => + route.fulfill({ status: 503, json: { code: 'LOCK_UNAVAILABLE', message: '门锁服务暂不可用' } }), ) await page.goto('/dorm/lock') await page.getByRole('button', { name: '修改动态码' }).click() await page.getByPlaceholder('请输入6位数字动态码').fill('654321') await page.getByRole('button', { name: '确定' }).click() - await expect(page.getByText('动态码修改过于频繁')).toBeVisible() + await expect(page.getByText('请求失败:HTTP 503')).toBeVisible() }) diff --git a/smart-h5/next.config.ts b/smart-h5/next.config.ts index 8a7a6fed..450d7717 100644 --- a/smart-h5/next.config.ts +++ b/smart-h5/next.config.ts @@ -85,10 +85,10 @@ const nextConfig: NextConfig = { const configuredTarget = process.env.API_PROXY_TARGET if (process.env.NODE_ENV !== 'development' && !configuredTarget) return [] const target = configuredTarget ?? 'https://xuchang.szyuto.com' - return GATEWAY_MODULES.map((module) => ({ + return [{ source: '/api/v1/:path*', destination: `${target}/api/v1/:path*` }, ...GATEWAY_MODULES.map((module) => ({ source: `/${module}/:path*`, destination: `${target}/${module}/:path*`, - })) + }))] }, // 给所有页面同时挂上:强制 CSP(仅三条零误报指令,立即拦截点击劫持 / base // 劫持 / 表单越权)+ 仅报告模式 CSP(继续观察 script/style 来源,不拦截)。 diff --git a/smart-h5/src/app/check-in/detail/page.tsx b/smart-h5/src/app/check-in/detail/page.tsx index 23ee060c..ecdc64f1 100644 --- a/smart-h5/src/app/check-in/detail/page.tsx +++ b/smart-h5/src/app/check-in/detail/page.tsx @@ -6,9 +6,7 @@ import { PageShell } from '@/components/page-shell' import { SegmentTabs } from '@/components/segment-tabs' import { useRequireAuth } from '@/features/auth/use-require-auth' import { getEmployeeBaseInfo } from '@/features/employee/api' -import { getLockPwd } from '@/features/dorm/api' import { getCheckInRecords } from '@/features/dorm-services/api' -import { decryptFromHex } from '@/lib/crypto/aes' /** Feature codes 1/3 = success, 0/2/4 = failure (legacy icon rule). */ function featureOk(code: number | undefined): boolean { @@ -25,15 +23,7 @@ function FeatureRow({ label, ok, desc }: { label: string; ok: boolean; desc?: st ) } -function safeDecrypt(cipher: string): string { - try { - return decryptFromHex(cipher) - } catch { - return '' - } -} - -/** Allocation result + door-lock identity-feature status. */ +/** 分配结果与门锁身份特征状态;密码仅能在本人门锁页完成二次核验后短时查看。 */ export default function CheckInDetailPage() { const authorized = useRequireAuth() @@ -49,17 +39,9 @@ export default function CheckInDetailPage() { queryFn: () => getCheckInRecords(badge as string), enabled: authorized && badge !== undefined, }) - const pwd = useQuery({ - queryKey: ['lock-pwd', badge], - queryFn: () => getLockPwd(badge as string), - enabled: authorized && badge !== undefined, - }) - if (!authorized) return null const rows = records.data?.code === 0 ? (records.data.data ?? []) : [] - const cipher = pwd.data?.code === 0 ? (pwd.data.data ?? '') : '' - const plainCode = cipher ? safeDecrypt(cipher) : '' const recordsFailed = records.isError || (records.isSuccess && records.data.code !== 0) return ( @@ -68,8 +50,8 @@ export default function CheckInDetailPage() { { - const [recordsResult, pwdResult] = await Promise.all([records.refetch(), pwd.refetch()]) - Toast.show(recordsResult.error || pwdResult.error ? '更新失败' : '更新成功') + const recordsResult = await records.refetch() + Toast.show(recordsResult.error ? '更新失败' : '更新成功') }} > {recordsFailed ? ( @@ -115,12 +97,6 @@ export default function CheckInDetailPage() { desc={record.lockPwd?.dynamicCode === 3 ? '已录入' : record.lockPwd?.dynamicDesc} /> - - {(record.lockPwd?.fingerprintCode ?? 0) !== 0 && plainCode && ( -

- {plainCode} -

- )} ))} diff --git a/smart-h5/src/app/dorm/get-code/page.test.ts b/smart-h5/src/app/dorm/get-code/page.test.ts index 739d81c8..784cbdfc 100644 --- a/smart-h5/src/app/dorm/get-code/page.test.ts +++ b/smart-h5/src/app/dorm/get-code/page.test.ts @@ -11,17 +11,22 @@ const routerMock = vi.hoisted(() => ({ replace: vi.fn(), })) +const searchMock = vi.hoisted(() => ({ value: '' })) + const visitorApiMock = vi.hoisted(() => ({ - checkFace: vi.fn(), faceCut: vi.fn(), })) const dormApiMock = vi.hoisted(() => ({ - refreshLockPwd: vi.fn(), + issueMyLockDynamicCodeVerification: vi.fn(), + requestMyLockDynamicCode: vi.fn(), + issueMyLockPasswordRevealVerification: vi.fn(), + revealMyLockPassword: vi.fn(), })) vi.mock('next/navigation', () => ({ useRouter: () => routerMock, + useSearchParams: () => new URLSearchParams(searchMock.value), })) vi.mock('next/image', async () => { @@ -56,12 +61,14 @@ vi.mock('@/features/employee/api', () => ({ })) vi.mock('@/features/visitor/api', () => ({ - checkFace: visitorApiMock.checkFace, faceCut: visitorApiMock.faceCut, })) vi.mock('@/features/dorm/api', () => ({ - refreshLockPwd: dormApiMock.refreshLockPwd, + issueMyLockDynamicCodeVerification: dormApiMock.issueMyLockDynamicCodeVerification, + requestMyLockDynamicCode: dormApiMock.requestMyLockDynamicCode, + issueMyLockPasswordRevealVerification: dormApiMock.issueMyLockPasswordRevealVerification, + revealMyLockPassword: dormApiMock.revealMyLockPassword, })) const stopTrack = vi.fn() @@ -86,9 +93,15 @@ function renderGetCodePage() { } beforeEach(() => { + searchMock.value = '' visitorApiMock.faceCut.mockResolvedValue({ code: 0, data: 'cut-face-base64' }) - visitorApiMock.checkFace.mockResolvedValue({ code: 0, data: { photoId: 'photo-1' } }) - dormApiMock.refreshLockPwd.mockResolvedValue({ code: 0 }) + dormApiMock.issueMyLockDynamicCodeVerification.mockResolvedValue({ + code: 0, + data: { verificationRef: 'verification-1' }, + }) + dormApiMock.requestMyLockDynamicCode.mockResolvedValue({ code: 0 }) + dormApiMock.issueMyLockPasswordRevealVerification.mockReset() + dormApiMock.revealMyLockPassword.mockReset() stopTrack.mockClear() getUserMedia.mockResolvedValue({ getTracks: () => [{ stop: stopTrack }] }) drawImage.mockClear() @@ -128,8 +141,8 @@ afterEach(() => { routerMock.push.mockClear() routerMock.replace.mockClear() visitorApiMock.faceCut.mockReset() - visitorApiMock.checkFace.mockReset() - dormApiMock.refreshLockPwd.mockReset() + dormApiMock.issueMyLockDynamicCodeVerification.mockReset() + dormApiMock.requestMyLockDynamicCode.mockReset() getUserMedia.mockReset() }) @@ -150,22 +163,56 @@ describe('门锁动态码刷新人脸采集', () => { expect(container.querySelector('input[type="file"]')).toBeNull() }) - it('generates the lock code with the captured face and employee badge', async () => { + it('uses service-side verification before the dynamic-code request without sending the employee badge', async () => { renderGetCodePage() fireEvent.click(await screen.findByTestId('lock-face-camera-start')) fireEvent.click(await screen.findByTestId('lock-face-camera-capture')) - expect(await screen.findByText('人脸识别通过')).toBeTruthy() + expect(await screen.findByText('人脸采集完成')).toBeTruthy() fireEvent.click(screen.getByTestId('lock-face-camera-generate')) await waitFor(() => - expect(dormApiMock.refreshLockPwd).toHaveBeenCalledWith({ - badge: 'YT027468', - facePic: 'cut-face-base64', - }), + expect(dormApiMock.issueMyLockDynamicCodeVerification).toHaveBeenCalledWith( + expect.objectContaining({ faceEvidence: 'cut-face-base64' }), + ), + ) + await waitFor(() => + expect(dormApiMock.requestMyLockDynamicCode).toHaveBeenCalledWith( + expect.objectContaining({ verificationRef: 'verification-1' }), + ), ) + expect(dormApiMock.issueMyLockPasswordRevealVerification).not.toHaveBeenCalled() + expect(dormApiMock.revealMyLockPassword).not.toHaveBeenCalled() await waitFor(() => expect(routerMock.replace).toHaveBeenCalledWith('/dorm/lock')) }) + + it('uses the fixed reveal route for the view flow and never falls back to refresh', async () => { + searchMock.value = 'flow=reveal' + dormApiMock.issueMyLockPasswordRevealVerification.mockResolvedValue({ + code: 0, + data: { verificationRef: 'verification-reveal' }, + }) + dormApiMock.revealMyLockPassword.mockResolvedValue({ code: 0, data: { password: '123456' } }) + renderGetCodePage() + + expect(await screen.findByRole('heading', { name: '查看动态码' })).toBeTruthy() + fireEvent.click(await screen.findByTestId('lock-face-camera-start')) + fireEvent.click(await screen.findByTestId('lock-face-camera-capture')) + fireEvent.click(await screen.findByTestId('lock-face-camera-generate')) + + await waitFor(() => + expect(dormApiMock.issueMyLockPasswordRevealVerification).toHaveBeenCalledWith( + expect.objectContaining({ faceEvidence: 'cut-face-base64' }), + ), + ) + await waitFor(() => + expect(dormApiMock.revealMyLockPassword).toHaveBeenCalledWith( + expect.objectContaining({ verificationRef: 'verification-reveal' }), + ), + ) + expect(dormApiMock.issueMyLockDynamicCodeVerification).not.toHaveBeenCalled() + expect(dormApiMock.requestMyLockDynamicCode).not.toHaveBeenCalled() + }) }) diff --git a/smart-h5/src/app/dorm/get-code/page.tsx b/smart-h5/src/app/dorm/get-code/page.tsx index d9f8d030..9d538236 100644 --- a/smart-h5/src/app/dorm/get-code/page.tsx +++ b/smart-h5/src/app/dorm/get-code/page.tsx @@ -1,21 +1,43 @@ 'use client' import { useQuery } from '@tanstack/react-query' import { Dialog, ErrorBlock } from 'antd-mobile' -import { useRouter } from 'next/navigation' -import { useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { Suspense, useState } from 'react' import { toUserMessage } from '@/lib/format/error-message' import { useRequireAuth } from '@/features/auth/use-require-auth' import { getEmployeeBaseInfo } from '@/features/employee/api' -import { refreshLockPwd } from '@/features/dorm/api' +import { + issueMyLockDynamicCodeVerification, + issueMyLockPasswordRevealVerification, + revealMyLockPassword, + requestMyLockDynamicCode, +} from '@/features/dorm/api' import { LockFaceCamera } from '@/features/dorm/lock-face-camera' import styles from './page.module.css' -/** 通过本机人脸核验后刷新宿舍门锁动态码。 */ +/** + * 查询参数仅用于选择本地展示的查看/刷新页面模式;Next 静态生成时必须由 Suspense 隔离 + * useSearchParams 的客户端渲染回退。 + */ export default function GetCodePage() { + return ( + + + + ) +} + +/** 采集人脸材料后,由服务端核验本人范围并刷新或短时查看宿舍门锁动态码。 */ +function GetCodeContent() { const authorized = useRequireAuth() const router = useRouter() - // facePic 是传给 checkFace 的 base64;常规 checkFace 响应只返回 data.photoId。 - const [facePic, setFacePic] = useState('') + const searchParams = useSearchParams() + // 此参数仅影响前端展示和选择固定端点;服务端仍由路由确定用途,不能相信客户端参数授权。 + const flow = searchParams.get('flow') === 'reveal' ? 'reveal' : 'dynamic' + const revealing = flow === 'reveal' + // 仅短时保存待服务端核验的材料;绝不从中派生人员、房间或“核验已通过”的客户端结论。 + const [faceEvidence, setFaceEvidence] = useState('') + const [captureVersion, setCaptureVersion] = useState(0) const [submitting, setSubmitting] = useState(false) const baseInfo = useQuery({ @@ -28,20 +50,54 @@ export default function GetCodePage() { const baseInfoFailed = baseInfo.isError || (baseInfo.isSuccess && baseInfo.data.code !== 0) async function handleGenerate() { - if (!facePic || !badge) return + if (!faceEvidence) return setSubmitting(true) try { - const res = await refreshLockPwd({ badge, facePic }) + const requestId = createRequestId() + const verification = revealing + ? await issueMyLockPasswordRevealVerification({ requestId, faceEvidence }) + : await issueMyLockDynamicCodeVerification({ requestId, faceEvidence }) + const verificationRef = readVerificationRef(verification) + if (verification.code !== 0 || !verificationRef) { + void Dialog.alert({ + title: '错误', + content: verification.msg ?? verification.message ?? '本人核验未通过,请重新拍照', + }) + return + } + + const res = revealing + ? await revealMyLockPassword({ + requestId, + idempotencyKey: `my-lock-reveal-${requestId}`, + verificationRef, + }) + : await requestMyLockDynamicCode({ + requestId, + idempotencyKey: `my-lock-dynamic-${requestId}`, + verificationRef, + }) if (res.code === 0) { - void Dialog.alert({ content: '刷新动态码成功!', confirmText: '确定' }).then(() => + const password = revealing ? readRevealedPassword(res) : undefined + if (revealing && !password) { + void Dialog.alert({ title: '错误', content: '动态码读取结果异常,请重新核验' }) + return + } + void Dialog.alert({ + content: revealing ? `当前动态码:${password}` : '刷新请求已受理,门锁生效结果待确认。', + confirmText: '确定', + }).then(() => router.replace('/dorm/lock'), ) } else { - void Dialog.alert({ title: '错误', content: res.msg ?? res.message ?? '刷新失败' }) + void Dialog.alert({ title: '错误', content: res.msg ?? res.message ?? (revealing ? '查看失败' : '刷新失败') }) } } catch (error) { - void Dialog.alert({ title: '错误', content: toUserMessage(error, '刷新失败') }) + void Dialog.alert({ title: '错误', content: toUserMessage(error, revealing ? '查看失败' : '刷新失败') }) } finally { + // 无论服务端是否接受,都不在页面状态中保留原始人脸材料或预览。 + setFaceEvidence('') + setCaptureVersion((version) => version + 1) setSubmitting(false) } } @@ -51,7 +107,7 @@ export default function GetCodePage() { if (baseInfoFailed) { return (
- router.back()} onClose={() => router.replace('/dorm/lock')} /> + router.back()} onClose={() => router.replace('/dorm/lock')} title={revealing ? '查看动态码' : '刷新动态码'} />
) } -function FaceScanTopbar({ onBack, onClose }: { onBack: () => void; onClose: () => void }) { +/** 当前浏览器只生成请求关联标识,不生成或推断任何权限范围。 */ +function createRequestId(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return `my-lock-${Date.now()}-${Math.random().toString(36).slice(2)}` +} + +/** 兼容门锁服务端以字符串或对象包装一次性引用的受控响应。 */ +function readVerificationRef(response: { data?: { verificationRef?: string } | string }): string | undefined { + if (typeof response.data === 'string') return response.data || undefined + return response.data?.verificationRef || undefined +} + +/** 查看结果只在当前异步栈中读取后传给短时弹窗,禁止写入 query cache、store 或页面状态。 */ +function readRevealedPassword(response: { data?: unknown }): string | undefined { + if (typeof response.data === 'string') return response.data || undefined + if (response.data && typeof response.data === 'object' && 'password' in response.data) { + const password = (response.data as { password?: unknown }).password + return typeof password === 'string' && password ? password : undefined + } + return undefined +} + +function FaceScanTopbar({ onBack, onClose, title }: { onBack: () => void; onClose: () => void; title: string }) { return (
-

刷新动态码

+

{title}

diff --git a/smart-h5/src/app/dorm/lock/page.tsx b/smart-h5/src/app/dorm/lock/page.tsx index 45eb2d58..4b07581c 100644 --- a/smart-h5/src/app/dorm/lock/page.tsx +++ b/smart-h5/src/app/dorm/lock/page.tsx @@ -3,24 +3,13 @@ import { useQuery } from '@tanstack/react-query' import { Dialog, ErrorBlock, SpinLoading, Toast } from 'antd-mobile' import { useRouter } from 'next/navigation' import { useEffect, useRef, useState } from 'react' +import { ApiError } from '@/lib/api/http' import { toUserMessage } from '@/lib/format/error-message' import { PageShell } from '@/components/page-shell' import { SMS_INPUT_CLASS } from '@/components/sms-code-field' import { useRequireAuth } from '@/features/auth/use-require-auth' -import { getEmployeeBaseInfo } from '@/features/employee/api' -import { getLockPwd, updateLockPwd } from '@/features/dorm/api' -import { decryptFromHex } from '@/lib/crypto/aes' - -/** Decrypt result with an explicit failure reason instead of silent ******. */ -function safeDecrypt(cipher: string): { code: string; error: string | null } { - try { - const plain = decryptFromHex(cipher) - return plain ? { code: plain, error: null } : { code: '', error: '动态码解析失败' } - } catch { - // getKey() throws when securityEncodeKey is missing — a deploy-config error. - return { code: '', error: '加密密钥未配置,请联系管理员' } - } -} +import { changeMyLockPassword, getMyLockStatus } from '@/features/dorm/api' +import { createLockStateController, resolveLockDisplayState, type LockStateController } from '@/features/dorm/lock-state' function RetryBlock({ description, onRetry }: { description: string; onRetry: () => void }) { return ( @@ -37,7 +26,7 @@ function RetryBlock({ description, onRetry }: { description: string; onRetry: () ) } -/** Door-lock dynamic code: decrypted display + change dialog + face refresh entry. */ +/** 本人门锁页:不自动读取或解密密码,敏感查看必须进入受控人脸核验流程。 */ export default function LockPage() { const authorized = useRequireAuth() const router = useRouter() @@ -45,100 +34,150 @@ export default function LockPage() { const [newPwd, setNewPwd] = useState('') const [submitting, setSubmitting] = useState(false) const notCheckedInRef = useRef(false) + const controllerRef = useRef(null) + + // 每次挂载独立创建,兼容 StrictMode 的清理后重新挂载。 + useEffect(() => { + const controller = createLockStateController() + controllerRef.current = controller + return () => { controller.destroy(); controllerRef.current = null } + }, []) - const baseInfo = useQuery({ - queryKey: ['employee', 'baseinfo'], - queryFn: getEmployeeBaseInfo, + const status = useQuery({ + queryKey: ['my-lock-status'], + queryFn: getMyLockStatus, enabled: authorized, }) - const badge = baseInfo.data?.code === 0 ? baseInfo.data.data?.employeeBadge : undefined - const pwd = useQuery({ - queryKey: ['lock-pwd', badge], - queryFn: () => getLockPwd(badge as string), - enabled: authorized && badge !== undefined, + const facts = status.data?.code === 0 ? status.data.data : undefined + const display = resolveLockDisplayState({ + hasCurrentMembership: facts?.hasCurrentMembership === true, + authorizationStatus: facts?.authorizationStatus, + capabilityStatus: facts?.capabilityStatus ?? 'UNVERIFIED', + cutoverFrozen: facts?.cutoverFrozen !== false, + dynamicCodeCapabilityVerified: facts?.dynamicCodeCapabilityVerified, + dynamicCodeCompliancePassed: facts?.dynamicCodeCompliancePassed, }) + const scopeRef = typeof facts?.scopeRef === 'string' && facts.scopeRef.trim() ? facts.scopeRef : null + const ready = authorized && status.isSuccess && !status.isFetching && !status.isError && !!scopeRef + const expectedVersion = typeof facts?.expectedVersion === 'string' + && /^[A-Za-z0-9_-]{1,128}$/.test(facts.expectedVersion) ? facts.expectedVersion : null + const canModify = ready && display.canModifyPassword && !!expectedVersion - const pwdFailed = pwd.isError || (pwd.isSuccess && pwd.data.code !== 0) - const cipher = pwd.data?.code === 0 ? (pwd.data.data ?? '') : '' - const decrypted = cipher ? safeDecrypt(cipher) : { code: '', error: null } + useEffect(() => { + const controller = controllerRef.current + if (!controller) return + // 即使住宿标识相同,状态重新读取也先废弃旧请求票据。 + controller.setScope(null) + controller.setFrozen(facts?.cutoverFrozen !== false) + if (!authorized) controller.logout() + else if (ready && display.canModifyPassword && scopeRef) controller.setScope({ scopeRef }) + return () => { controller.setScope(null) } + }, [authorized, ready, scopeRef, facts, display.canModifyPassword]) - // Not checked in: the gateway returns an empty pwd with a success code. + // 是否有当前住宿只能由服务端状态决定,不能从旧密码是否为空推断。 useEffect(() => { - if (pwd.isSuccess && pwd.data.code === 0 && !pwd.data.data && !notCheckedInRef.current) { + if (status.isSuccess && status.data.code === 0 && !status.data.data?.hasCurrentMembership && !notCheckedInRef.current) { notCheckedInRef.current = true void Dialog.alert({ content: '您暂未入住智能宿舍,请联系宿管入住!', confirmText: '确定', }).then(() => router.replace('/dorm')) } - }, [pwd.isSuccess, pwd.data, router]) - - useEffect(() => { - if (decrypted.error) Toast.show(decrypted.error) - }, [decrypted.error]) + }, [router, status.data, status.isSuccess]) async function handleEditSubmit() { - if (submitting) return + if (submitting || !canModify || !expectedVersion) return if (!/^[0-9]{6}$/.test(newPwd)) { Toast.show('请输入6位数字动态码') return } - if (newPwd === decrypted.code) { - Toast.show('请输入跟当前动态码不一样的新的动态码') - return - } + const controller = controllerRef.current + const operation = controller?.beginPasswordChange() + if (!controller || !operation?.accepted) return setSubmitting(true) try { - const res = await updateLockPwd({ badge: badge as string, newPwd }) + const requestId = createRequestId() + const res = await changeMyLockPassword({ + requestId, + idempotencyKey: `my-lock-change-${requestId}`, + password: newPwd, + expectedVersion, + }) + // 卸载、冻结、调宿或重新查询之后的迟到结果不可继续操作界面。 + if (!controller.finish(operation.ticket)) return if (res.code === 0) { setEditVisible(false) setNewPwd('') - await pwd.refetch() + void Dialog.alert({ content: '修改请求已提交,请以处理结果为准', confirmText: '确定' }) } else { void Dialog.alert({ title: '错误', content: res.msg ?? res.message ?? '修改失败' }) } } catch (error) { - void Dialog.alert({ title: '错误', content: toUserMessage(error, '修改失败') }) + if (!controller.finish(operation.ticket)) return + if (error instanceof ApiError && error.status === 409) { + // 并发冲突只刷新事实,不自动重试密码写入。 + setEditVisible(false) + void status.refetch() + void Dialog.alert({ title: '状态已变化', content: '请核对最新门锁状态后重新操作。' }) + } else { + void Dialog.alert({ title: '错误', content: toUserMessage(error, '修改失败') }) + } } finally { - setSubmitting(false) + if (controllerRef.current === controller) { + setNewPwd('') + setSubmitting(false) + } } } if (!authorized) return null - const baseInfoFailed = baseInfo.isError || (baseInfo.isSuccess && baseInfo.data.code !== 0) + const statusFailed = status.isError || (status.isSuccess && status.data.code !== 0) return ( - {baseInfoFailed ? ( - void baseInfo.refetch()} /> - ) : pwdFailed ? ( + {statusFailed ? ( void pwd.refetch()} + description={status.data?.message ?? status.data?.msg ?? '门锁状态获取失败'} + onRetry={() => void status.refetch()} /> - ) : baseInfo.isPending || pwd.isPending ? ( + ) : status.isPending ? (
) : (

你的门锁动态码

+

{scopeRef ? display.text : '门锁状态待核实'}

- {decrypted.code || '******'} + ****** +

+ +

+ 为保护宿舍安全,请完成人脸核验后短时查看当前动态码。

+ +