From 8cef743ed670ac442bc27e2ac80862490ba635e7 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:14 +0800 Subject: [PATCH 1/8] fix: add device payload contract test script (#56) --- Sense/ui/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sense/ui/package.json b/Sense/ui/package.json index 11f00ba..dd9ca90 100644 --- a/Sense/ui/package.json +++ b/Sense/ui/package.json @@ -11,7 +11,8 @@ "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "eslint \"src/**/*.{js,vue}\"", - "test:navigation": "node --test tests/navigation.test.cjs" + "test:navigation": "node --test tests/navigation.test.cjs", + "test:device-payload": "node --test tests/devicePayload.test.mjs" }, "dependencies": { "@element-plus/icons-vue": "2.3.2", @@ -51,3 +52,4 @@ "not dead" ] } + -- 2.34.1 From b0ea0180754f93bd8b7ad79b379686379c877e5d Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:15 +0800 Subject: [PATCH 2/8] fix: use explicit device request payloads (#56) --- Sense/ui/src/views/sense/device/Devices.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sense/ui/src/views/sense/device/Devices.vue b/Sense/ui/src/views/sense/device/Devices.vue index 43b4090..6a04ce9 100644 --- a/Sense/ui/src/views/sense/device/Devices.vue +++ b/Sense/ui/src/views/sense/device/Devices.vue @@ -52,6 +52,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Search } from '@element-plus/icons-vue' import { useStore } from 'vuex' import { createDevice, disableDevice, listDevices, updateCredential, updateDevice } from '../../../api/sense/device' +import { createDevicePayload, updateDevicePayload } from './devicePayload.mjs' const store = useStore() const loading = ref(false), saving = ref(false), deviceDialog = ref(false), credentialDialog = ref(false) @@ -69,7 +70,7 @@ function reset() { Object.assign(query, { keyword: '', page: 1, page_size: 20 }) function openCreate() { editing.value = null; Object.assign(deviceForm, { name: '', location: '', modality: 'video', capabilities: ['video'] }); deviceDialog.value = true } function openEdit(row) { editing.value = row; Object.assign(deviceForm, { name: row.name, location: row.location, modality: row.modality, capabilities: row.capabilities }); deviceDialog.value = true } function openCredential(row) { credentialTarget.value = row; Object.assign(credentialForm, { onvif_username: '', onvif_password: '', rtsp_same_as_onvif: row.rtsp_credential_same_as_onvif !== false, rtsp_username: '', rtsp_password: '' }); credentialDialog.value = true } -async function saveDevice() { const valid = await deviceFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { if (editing.value) await updateDevice(editing.value.id, { ...deviceForm, version: editing.value.version }); else await createDevice(deviceForm); ElMessage.success('设备已保存'); deviceDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '保存失败') } finally { saving.value = false } } +async function saveDevice() { const valid = await deviceFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { if (editing.value) await updateDevice(editing.value.id, updateDevicePayload(deviceForm, editing.value.version)); else await createDevice(createDevicePayload(deviceForm)); ElMessage.success('设备已保存'); deviceDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '保存失败') } finally { saving.value = false } } async function saveCredential() { const valid = await credentialFormRef.value?.validate().catch(() => false); if (!valid) return; saving.value = true; try { await updateCredential(credentialTarget.value.id, credentialForm); Object.assign(credentialForm, { onvif_username: '', onvif_password: '', rtsp_same_as_onvif: true, rtsp_username: '', rtsp_password: '' }); ElMessage.success('ONVIF 与 RTSP 凭据已安全更新'); credentialDialog.value = false; await load() } catch (error) { ElMessage.error(error.message || '更新失败') } finally { saving.value = false } } async function disable(row) { await ElMessageBox.confirm(`停用“${row.name}”后将停止后续接入,设备记录和审计仍保留。`, '确认停用', { type: 'warning' }); await disableDevice(row.id, row.version); ElMessage.success('设备已停用'); await load() } onMounted(load) -- 2.34.1 From 949e50f1c5f9666a4b4974dfeee8740f98442d8b Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:15 +0800 Subject: [PATCH 3/8] fix: separate create and update device payloads (#56) --- .../src/views/sense/device/devicePayload.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Sense/ui/src/views/sense/device/devicePayload.mjs diff --git a/Sense/ui/src/views/sense/device/devicePayload.mjs b/Sense/ui/src/views/sense/device/devicePayload.mjs new file mode 100644 index 0000000..c101a58 --- /dev/null +++ b/Sense/ui/src/views/sense/device/devicePayload.mjs @@ -0,0 +1,18 @@ +export function createDevicePayload(form) { + return { + name: form.name, + location: form.location, + modality: form.modality, + capabilities: [...form.capabilities] + } +} + +export function updateDevicePayload(form, version) { + return { + name: form.name, + location: form.location, + capabilities: [...form.capabilities], + version + } +} + -- 2.34.1 From e18502ee67f72c3f8c019e7bd59b86d540c8f725 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:16 +0800 Subject: [PATCH 4/8] test: cover device payload field boundaries (#56) --- Sense/ui/tests/devicePayload.test.mjs | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Sense/ui/tests/devicePayload.test.mjs diff --git a/Sense/ui/tests/devicePayload.test.mjs b/Sense/ui/tests/devicePayload.test.mjs new file mode 100644 index 0000000..9c8c2eb --- /dev/null +++ b/Sense/ui/tests/devicePayload.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { createDevicePayload, updateDevicePayload } from '../src/views/sense/device/devicePayload.mjs' + +const form = { + name: '东门摄像机', + location: '教学楼一楼东门', + modality: 'video', + capabilities: ['video'], + id: 'read-only-id' +} + +test('create payload keeps Chinese values and includes modality', () => { + assert.deepEqual(createDevicePayload(form), { + name: '东门摄像机', + location: '教学楼一楼东门', + modality: 'video', + capabilities: ['video'] + }) +}) + +test('update payload uses the PATCH allowlist and excludes modality', () => { + const payload = updateDevicePayload(form, 9) + assert.deepEqual(payload, { + name: '东门摄像机', + location: '教学楼一楼东门', + capabilities: ['video'], + version: 9 + }) + assert.equal('modality' in payload, false) + assert.equal('id' in payload, false) +}) + -- 2.34.1 From b2cf37cccb1ead29d4f4c6fcaf14ba2a573828c8 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:17 +0800 Subject: [PATCH 5/8] docs: document device edit request boundary (#56) --- docs/03-business-rules-and-glossary.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index db55f39..8b020f1 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.- -wiki_revision: 06b68d16bf907decdaf85a813d417896f7920f4c -synchronized_at: 2026-08-13T14:06:00Z +wiki_revision: f9c089d2b993478e586f4abdb1845952a3cd3c51 +synchronized_at: 2026-08-13T14:37:00Z # 业务规则与术语 @@ -97,4 +97,8 @@ synchronized_at: 2026-08-13T14:06:00Z - **close**:只有处置人或管理员可以完成;必须选择“确认有危险、误报、现场正常、无法确认”之一,可附备注。相同重复请求幂等,其他改变结果的请求被拒绝。 - **审计事实**:成功生命周期事实只追加;失败、重复和拒绝尝试写入安全审计,但不记录令牌、密码或连接密钥。 +## 设备编辑请求边界 + +- 新建设备请求显式提交名称、安装位置、设备类型和能力;编辑设备请求只提交名称、安装位置、能力和版本,不提交新建专用的 `modality` 或响应中的只读字段。 +- 设备名称和安装位置允许 UTF-8 中文;后端继续严格拒绝未知 JSON 字段,前端必须使用字段白名单构造请求,不得直接展开整个表单或响应对象。 -- 2.34.1 From 6a00d25557a8448df8b2978e4c5f727efa368e58 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:17 +0800 Subject: [PATCH 6/8] docs: add device edit invalid request troubleshooting (#56) --- docs/06-troubleshooting.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index 70f620f..43b17a0 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting -wiki_revision: b70e92180546609cb9b3b05b2c13c1a49676c03b -synchronized_at: 2026-08-13T14:05:00Z +wiki_revision: 7deb549d8b0ae87cdca0c7494e36150d261248a1 +synchronized_at: 2026-08-13T14:37:00Z # 故障排查 @@ -76,4 +76,9 @@ synchronized_at: 2026-08-13T14:05:00Z `demo` 只用于临时查看。生产数据持久性、真实设备和媒体链路不能用 demo 验证替代。 +## 设备管理 + +| 现象 | 处理 | +|---|---| +| 编辑设备时提示“请求内容格式不正确” | 中文名称和安装位置本身受支持。检查 PATCH payload 是否误带只允许新建时提交的 `modality` 或其他未知字段;编辑请求应只包含 `name`、`location`、`capabilities`、`version`。不要通过放宽后端未知字段校验绕过前端契约错误。 | -- 2.34.1 From e2d1618b46b0d6d7864734815d2d2c860f21eb48 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:18 +0800 Subject: [PATCH 7/8] docs: archive task #56 --- ...备编辑请求包含新建专用字段.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/task/56-修复设备编辑请求包含新建专用字段.md diff --git a/docs/task/56-修复设备编辑请求包含新建专用字段.md b/docs/task/56-修复设备编辑请求包含新建专用字段.md new file mode 100644 index 0000000..7fc6f37 --- /dev/null +++ b/docs/task/56-修复设备编辑请求包含新建专用字段.md @@ -0,0 +1,67 @@ + +generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) +wiki_page: Task-56-修复设备编辑请求包含新建专用字段 +wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-56-%E4%BF%AE%E5%A4%8D%E8%AE%BE%E5%A4%87%E7%BC%96%E8%BE%91%E8%AF%B7%E6%B1%82%E5%8C%85%E5%90%AB%E6%96%B0%E5%BB%BA%E4%B8%93%E7%94%A8%E5%AD%97%E6%AE%B5.- +wiki_revision: 212cf0a998afb41adfb1d671df07a1362580ea9e +synchronized_at: 2026-08-13T14:37:31Z + + +# 56 修复设备编辑请求包含新建专用字段 + +- 类型:缺陷 +- 所属 Epic:#7 +- 所属 MVP / 版本:#8 +- 状态:待验收 +- 日期:2026-08-13 +- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/56 +- Wiki 页面:Task-56-修复设备编辑请求包含新建专用字段 +- Wiki revision:见本地镜像头 + +## 背景与目标 + +用户在设备管理编辑中文安装位置时收到“请求内容格式不正确”。中文 UTF-8 并非根因;前端编辑请求展开了整个表单,把只允许新建时提交的 `modality` 发送到严格拒绝未知字段的 PATCH 接口。目标是让新建和编辑请求分别遵循明确字段白名单,并用可运行测试固定契约。 + +## 最终方案 + +- 提取纯 payload 构造函数,避免页面直接展开表单对象。 +- 新建请求显式包含 `name`、`location`、`modality`、`capabilities`。 +- 编辑请求显式仅包含 `name`、`location`、`capabilities`、`version`。 +- 使用 Node 内置测试运行器验证中文值原样保留、新建包含设备类型、编辑排除设备类型和只读 ID。 +- 后端 `DisallowUnknownFields` 保持不变,继续把未知字段视为契约错误。 + +## 修改文件 + +- `Sense/ui/src/views/sense/device/Devices.vue`:分别构建设备新建和编辑请求。 +- `Sense/ui/src/views/sense/device/devicePayload.mjs`:设备请求字段白名单。 +- `Sense/ui/tests/devicePayload.test.mjs`:中文值和字段边界测试。 +- `Sense/ui/package.json`:增加设备 payload 测试命令。 +- Wiki 业务规则与故障排查页面。 + +## 验收结果 + +| 验收标准 | 结果 | +|---|---| +| 编辑设备保存中文安装位置 | 通过,PATCH 返回 200 | +| 保存后回读中文安装位置 | 通过,回读为“教学楼一楼东门” | +| 编辑请求排除 modality 和只读字段 | 通过,单元测试验证 | +| 新建请求保留 modality | 通过,单元测试验证 | +| 后端严格未知字段策略保持不变 | 通过,未修改后端解析逻辑 | + +## 测试 + +- `corepack pnpm@9.15.1 test:device-payload`:2 项通过。 +- `corepack pnpm@9.15.1 lint`:0 error,806 个既有 warning。 +- `corepack pnpm@9.15.1 build`:通过,3 个既有体积 warning。 +- `go test ./...`:通过。 +- Go 1.26.5 Windows 重新打包并启动:通过。 +- 真实 PostgreSQL API:白名单 PATCH 保存成功,GET 回读中文位置成功。 +- **未验证部分**:最终浏览器点击“保存”的视觉交互由用户验收;请求构造、真实 API 保存和回读已自动验证。 + +## 遗留问题 + +- 严格 Harness 仍受既有 #44 归档缺少“最终方案”章节影响。 + +## 相关提交 + +- `8e3a25d` 修复设备编辑请求字段并增加契约测试。 + -- 2.34.1 From 014085c8e39a2160e123a8b3e2039d943058c1e0 Mon Sep 17 00:00:00 2001 From: ila <2+ila@noreply.git.ilapage.cn> Date: Thu, 13 Aug 2026 22:40:19 +0800 Subject: [PATCH 8/8] docs: register task #56 archive --- wiki-docs.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wiki-docs.json b/wiki-docs.json index f8ccd6a..3cdc870 100644 --- a/wiki-docs.json +++ b/wiki-docs.json @@ -147,6 +147,10 @@ { "page": "Task-54-修复实时监看按需拉流循环等待", "path": "docs/task/54-修复实时监看按需拉流循环等待.md" + }, + { + "page": "Task-56-修复设备编辑请求包含新建专用字段", + "path": "docs/task/56-修复设备编辑请求包含新建专用字段.md" } ] } -- 2.34.1