fix(admin): 局域网大屏卡片按「地址类别」判定 + 三层兜底 + 手填入口(dev_0.5.0)
## 判断失误的修正(主人现场实测出来的) 主人现场是 **169.254 自动配置网段**(没有 DHCP,设备靠链路本地地址互访), 平板实测能访问 `http://169.254.79.190:19540/display`;而卡片一开始把链路本地地址 **排除**了,于是显示「未检测到局域网地址」。 根因:`usableLanIPv4` 排除了 `IsLinkLocalUnicast()`,理由写的是「169.254 说明没拿到 IP」 ——那是**对网络健不健康的猜测**,不是**地址能不能用的事实**。 现改为按**地址类别**判定(`ipv4Scope`,全部用标准库谓词,**代码里没有任何网段字面量**): 只排除「作为对端访问目标没有意义」的四类(回环 / 未指定 / 组播 / 广播), 链路本地与私网、全局一样进候选,只**排在最后**并带说明 「自动配置地址:本网段没有 DHCP,同网段设备可以互相访问」。 新增**反硬编码护栏用例**:函数体内不得出现 `x.x.x.x` 字面量,且必须用满 6 个谓词。 顺带修掉新用例抓出的真 bug:`ipv4Scope(nil)` 曾落进 default 被当成「可用的 global」, 而 IPv6 的 `To4()` 正是 nil → 「只有 IPv6 的网卡」会被误判成有可用地址。 ## 三层兜底(离线现场必须拿得到地址) 1. **严格轮**:接口 up 且非回环; 2. **宽松轮**:严格轮为空时不再要求 up 位(某些驱动/虚拟网卡读不到),命中标 `confidence: low`; 3. **默认路由探测**:两轮皆空时用 `net.Dial("udp4", …)` 问出本机源地址 (UDP connect 只查路由表、不握手不发包,离线安全;TCP 会等 SYN 超时,不能用),标 `confidence: route`。 接口同时回 `scope`(private/global/link-local)与 `skipped`(被跳过的网卡 + 原因, code 收敛为 no-address / only-ipv6 / not-up / loopback——link-local 不再是「被跳过」的理由)。 ## 手填入口(最后一道保险) 自动检测不到、或不是现场要用的网段时,操作员把 `ipconfig` 里的地址填进卡片, 立刻生成链接 + 二维码;排在第一条、带「手动」徽标、可删除、localStorage 持久化、 四条就地校验文案(空/含空格/不像 IP/重复),IPv6 也支持。 ## 自检工具 `debug/诊断/局域网地址自检.exe` + 说明:只读、不写文件、不需管理员, 打印系统给 Go 看的网卡列表(含类别)、默认路由探测结果与可用大屏地址。 (正是它把现场定性成 169.254 网段的。) ## 真浏览器验证(四场景,非源码断言) 链接本地地址出链接+二维码+说明句 / 空态列出被跳过网卡并折叠(5 条 + 等共 N 块)/ low 与 route 两种来源提示各占一行互不顶掉 / 手填地址排第一并刷新不丢 —— 全部通过,0 页面错误。 ## 文档 使用手册 §9、部署指南 7.5/7.9、上线检查清单(**防火墙放行入站 19540** 与 「另一台设备能打开局域网大屏」两条新增)、常见问题 Q101、升级说明 dev_0.5.0; devdocs 05/07/13 同步(含这次误判的来龙去脉与「Get-NetAdapter 显示 Up ≠ 有 IP」)。 **没有数据迁移**,旧赛事包照常导入。 自检:服务端 13 包 ×2;前端 vue-tsc + 680 例 ×2 + build;Console 20 文件 + 68 例。
This commit is contained in:
1 parent
9ef68c2589
commit
c97d3150c9
18 files changed
+2425
-79
No files matched your search
@@ -27,7 +27,9 @@ import type {
|
||||
ImportConflictDecision,
|
||||
LiveEvents,
|
||||
NetworkAddress,
|
||||
NetworkAddressScope,
|
||||
NetworkInfo,
|
||||
NetworkSkippedInterface,
|
||||
Page,
|
||||
PageQuery,
|
||||
PointAdjustment,
|
||||
@@ -721,6 +723,30 @@ function readNetworkAddress(value: unknown): NetworkAddress | null {
|
||||
ip,
|
||||
interface: readString(value, 'interface') ?? '',
|
||||
family: readString(value, 'family') ?? 'ipv4',
|
||||
// 只认服务端契约里的 low / route 两个「需要额外提示」的值:老服务端没有这个字段
|
||||
// (它的地址本来就走严格轮),值也不认识时按 high 处理——凭一个没见过的值在卡片上
|
||||
// 多印一句提示,是凭空造出来的噪声。
|
||||
confidence: value.confidence === 'low' || value.confidence === 'route' ? value.confidence : 'high',
|
||||
// 类别同样只认契约里的三个值;认不出(老服务端)时留 undefined,卡片就不加类别说明。
|
||||
// 绝不按 IP 前缀在这里补一个:分类是服务端的知识,前端猜出来的值会和它打架。
|
||||
scope: readScope(value.scope),
|
||||
}
|
||||
}
|
||||
|
||||
function readScope(value: unknown): NetworkAddressScope | undefined {
|
||||
return value === 'private' || value === 'global' || value === 'link-local' ? value : undefined
|
||||
}
|
||||
|
||||
/** 归一化 `skipped[]` 的一条:没有 code 的行解释不了任何事,直接丢掉。 */
|
||||
function readNetworkSkipped(value: unknown): NetworkSkippedInterface | null {
|
||||
if (!isRecord(value)) return null
|
||||
const code = readString(value, 'code')
|
||||
if (code === null || code === '') return null
|
||||
const rawAddresses = Array.isArray(value['addresses']) ? value['addresses'] : []
|
||||
return {
|
||||
interface: readString(value, 'interface') ?? '',
|
||||
code,
|
||||
addresses: rawAddresses.filter((item): item is string => typeof item === 'string'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,10 +756,13 @@ function readNetworkAddress(value: unknown): NetworkAddress | null {
|
||||
* 端口/路径是拼地址的原料,前端不替服务端猜:缺端口就留 0,由页面判断「读不到可用地址」
|
||||
* 并给说明——编一个默认端口出来会得到一条看着对、实际打不开的链接。
|
||||
* addresses 不是数组时按「没检测到地址」处理,交给页面显示空态。
|
||||
* skipped 不是数组时归一化成空数组(等价于「没有可解释的网卡」):老服务端压根不返回这个
|
||||
* 字段,卡片据此退回原来那句空态文案,行为与升级前一致。
|
||||
*/
|
||||
function readNetworkInfo(value: unknown): NetworkInfo {
|
||||
const port = firstNumberOrNull(isRecord(value) ? value['port'] : null)
|
||||
const rawAddresses = isRecord(value) && Array.isArray(value['addresses']) ? value['addresses'] : []
|
||||
const rawSkipped = isRecord(value) && Array.isArray(value['skipped']) ? value['skipped'] : []
|
||||
return {
|
||||
port: port !== null && port > 0 ? Math.floor(port) : 0,
|
||||
displayPath: normalizePath(readString(value, 'displayPath'), '/display'),
|
||||
@@ -741,6 +770,9 @@ function readNetworkInfo(value: unknown): NetworkInfo {
|
||||
addresses: rawAddresses
|
||||
.map(readNetworkAddress)
|
||||
.filter((item): item is NetworkAddress => item !== null),
|
||||
skipped: rawSkipped
|
||||
.map(readNetworkSkipped)
|
||||
.filter((item): item is NetworkSkippedInterface => item !== null),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user