news 2026/9/8 8:14:51

游戏开发实战:如何提前显示关卡怪物已捕获状态?详解表设计与左连接

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
游戏开发实战:如何提前显示关卡怪物已捕获状态?详解表设计与左连接

最近在做一个收集养成玩法时,遇到了一个非常典型的需求场景:“以防你不知道,可以提前知道这关的怪你有没有抓到过。”

这句话看起来像一句绕口令,但放到项目里其实是一个很具体的功能:玩家进入关卡之前,界面上就要能显示出当前关卡里出现的怪物,自己以前有没有捕获过。如果捕获过,就显示“已捕获”;如果没捕获过,就不显示或者显示“未捕获”。很多图鉴收集、关卡刷怪类的游戏都有类似需求。

这个功能说起来不复杂,真正落地时却涉及数据表设计、左连接查询、状态幂等写入、前端展示、缓存兼容等一系列问题。下面我把这套完整实现思路拆开讲一遍,代码、SQL、排错都会覆盖到。

1. 背景与核心概念

1.1 这个需求到底要解决什么

我先说一个直观场景。

玩家进入“第 3 关”前,界面右侧展示出这个关卡会出现的怪物列表。玩家想知道自己以前刷这关时,有没有抓过其中某只怪。如果抓过,他想直接跳过,不愿意再浪费时间重复刷;如果没有抓过,他就有目标地进本。

这里的核心不是“能不能抓到”,而是“抓没抓到过”。

所以功能上要解决两件事:

  1. 关卡与怪物之间要有确定的关联关系。
  2. 玩家与怪物之间的捕获关系要能被查询出来。

一句话总结:系统需要在进入关卡前,把“该关卡下的怪物列表”和“当前玩家已经捕获的怪物集合”做一次匹配,再把匹配结果以状态的形式展示给玩家。

1.2 数据维度拆解

这个需求里其实存在三个数据维度:

  • 怪物维度:怪物 ID、怪物名称、所属关卡。
  • 玩家维度:用户 ID。
  • 捕获关系维度:哪个玩家在哪只怪物上形成了“已捕获”记录。

从表面上看,只需要给怪物表加一个字段,比如user_id,表示这个用户抓住了这只怪。但仔细一想就会发现,这种设计完全不能复用。怪物是公共数据,玩家是个体数据,两个人的捕获记录不能写在同一条怪物记录上。

正确的数据设计思路是:

  • 怪物表:存放全局怪物信息以及它所属的关卡。
  • 玩家捕获表:记录“哪个玩家捕获了哪只怪物”。
  • 通过monster_id把两张表关联起来。

这也是绝大多数收集类游戏的标准做法。

1.3 常见概念误区

在实际开发中,最容易混淆的是两个状态:

  • 已见过:玩家在图鉴、战斗或者剧情里看到过这只怪物。
  • 已捕获:玩家主动操作后,怪物成为玩家的收集物。

以“提前知道这关的怪你有没有抓到过”这个需求来说,业务上要的是已捕获,不是已见过。如果只记录“遇到过”,那玩家进入关卡前看到“已捕获”的提示,到了战斗里却发现这只怪连捕获条件都还没触发,体验就很奇怪。

所以捕获记录的写入时机非常重要,必须放在真正的捕获事件成功之后,不能放在进入战斗或者看到怪物时。

2. 环境准备与数据结构设计

2.1 技术选型说明

这个功能并没有非常强的技术栈绑定,后端、前端、数据库都有多种方案。常见组合有:

  • 后端使用 Java + Spring Boot,接口层提供查询和写入。
  • 数据库使用 MySQL,负责存储怪物和捕获关系。
  • 前端使用简单的 HTML + JavaScript 做状态展示,也可以换成 Vue、React。
  • 如果玩家数量大,还可以在服务层叠加 Redis 缓存。

本文以 Spring Boot + MyBatis + MySQL 为例,重点是配置思路和代码结构。版本可以根据你的项目实际情况调整,不用照搬某一个固定版本,只要配套关系正确即可。

2.2 数据表设计

核心表是两张:monsteruser_capture

先看怪物表。

CREATE TABLE monster ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '怪物ID', name VARCHAR(64) NOT NULL COMMENT '怪物名称', level_id BIGINT NOT NULL COMMENT '所属关卡ID', PRIMARY KEY (id), KEY idx_level (level_id) ) ENGINE = InnoDB COMMENT '怪物表';

这里把level_id作为怪物所属关卡的标识。一个关卡可以有多只怪物,一只怪物也可以出现在多个关卡,如果业务是一对多关系,可以单独建一张level_monster关联表。为了演示简洁,这里直接用monster.level_id表示所属关卡。

再看玩家捕获表。

CREATE TABLE user_capture ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '玩家ID', monster_id BIGINT NOT NULL COMMENT '怪物ID', captured_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '捕获时间', PRIMARY KEY (id), UNIQUE KEY uk_user_monster (user_id, monster_id), KEY idx_monster (monster_id) ) ENGINE = InnoDB COMMENT '玩家捕获记录表';

这里的关键点有两个:

  1. uk_user_monster (user_id, monster_id)是唯一索引,保证同一个玩家对同一只怪物只能有一条捕获记录,避免重复插入。
  2. 捕获记录表只存“成功捕获”这一动作,不存“未捕获态”。未捕获是一个默认状态,通过查不到记录来表示。

2.3 表结构设计的取舍

可能有人会问:为什么不在monster表里加一个capture_count或者user_id

原因很简单:

  • 全局怪物数据属于公共配置,不应该被玩家个性化状态污染。
  • 一张怪物表如果既要存公共属性,又要存每个玩家的捕获状态,数据量和维护成本都会失控。
  • 单独建user_capture表后,后续扩展非常方便,比如记录捕获时间、捕获方式、是否进图鉴、是否使用过该怪物等。

所以表结构上采用“公共数据 + 用户行为记录”的拆分方式,这也是最稳妥的设计。

3. 核心逻辑实现

3.1 查询状态:左连接匹配

要查询某个关卡下所有怪物的捕获状态,SQL 思路是:

  1. 从怪物表查出该关卡的所有怪物。
  2. 把当前玩家的捕获记录作为关联表挂上去。
  3. 能关联上的怪物,说明已捕获;不能关联上的,说明未捕获。

SQL 可以写成这样:

SELECT m.id AS monster_id, m.name AS monster_name, CASE WHEN uc.id IS NULL THEN 0 ELSE 1 END AS captured FROM monster m LEFT JOIN user_capture uc ON uc.monster_id = m.id AND uc.user_id = #{userId} WHERE m.level_id = #{levelId} ORDER BY m.id;

这里最容易出错的是LEFT JOINON条件。

捕获记录表的两个条件都应该放在ON中,而不是把uc.user_id = #{userId}放到WHERE中。如果放到WHERE里,那些未匹配到的行会被过滤掉,最终结果就只剩已捕获的怪物,没有捕获的怪物反而不显示了,这就不符合需求。

3.2 写入状态:幂等插入

捕获动作的写入是高频操作,玩家可能因为网络重试、按钮双击等原因,在同一时间对同一只怪物发起多次捕获请求。如果每次都插入一条新纪录,就会出现脏数据。

所以插入时要用唯一索引做幂等控制:

INSERT INTO user_capture(user_id, monster_id, captured_at) VALUES (#{userId}, #{monsterId}, NOW()) ON DUPLICATE KEY UPDATE captured_at = captured_at;

ON DUPLICATE KEY UPDATE captured_at = captured_at表示:如果唯一索引冲突,说明这条捕获记录已经存在,这时不产生新数据,只保持原记录不变。

3.3 前端展示规则

查询接口返回的数据是一个怪物状态列表,前端只需要根据captured字段判断即可。

展示规则通常如下:

  • captured = true:在卡片上显示“已捕获”,可以加绿色边框或角标。
  • captured = false:只展示怪物名称或图鉴图标,不显示“已捕获”标记。
  • 如果产品不希望提前暴露未捕获怪物的完整信息,可以让未捕获怪物的名称以“???”形式展示,只告诉玩家“这个关卡的怪物有没有收集完”,而不展示具体怪物名。

技术上是完全可行的,细节取决于产品交互设计。

4. 完整实战案例

下面用一个完整案例,把上面这套逻辑串起来。

4.1 项目结构

假设项目名为game-track,结构如下:

game-track/ ├── pom.xml ├── sql/ │ └── init.sql └── src/main/ ├── java/com/example/gametrack/ │ ├── GameTrackApplication.java │ ├── controller/MonsterController.java │ ├── mapper/MonsterMapper.java │ ├── model/MonsterStatusVO.java │ └── service/MonsterCaptureService.java └── resources/ ├── application.properties └── mapper/MonsterMapper.xml

4.2 初始化数据库脚本

文件路径:sql/init.sql

CREATE DATABASE IF NOT EXISTS game_db DEFAULT CHARACTER SET utf8mb4; USE game_db; DROP TABLE IF EXISTS user_capture; DROP TABLE IF EXISTS monster; CREATE TABLE monster ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '怪物ID', name VARCHAR(64) NOT NULL COMMENT '怪物名称', level_id BIGINT NOT NULL COMMENT '所属关卡ID', PRIMARY KEY (id), KEY idx_level (level_id) ) ENGINE = InnoDB COMMENT '怪物表'; CREATE TABLE user_capture ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '玩家ID', monster_id BIGINT NOT NULL COMMENT '怪物ID', captured_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '捕获时间', PRIMARY KEY (id), UNIQUE KEY uk_user_monster (user_id, monster_id), KEY idx_monster (monster_id) ) ENGINE = InnoDB COMMENT '玩家捕获记录表'; INSERT INTO monster (name, level_id) VALUES ('史莱姆', 1), ('野猪', 1), ('蝙蝠', 1), ('幽灵', 2), ('骷髅', 2);

这里有两关:第 1 关有 3 只怪,第 2 关有 2 只怪。后面验证时,可以让玩家 10001 捕获第 1 关的史莱姆,然后查询效果。

4.3 后端代码

首先是启动类。

package com.example.gametrack; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.gametrack.mapper") public class GameTrackApplication { public static void main(String[] args) { SpringApplication.run(GameTrackApplication.class, args); } }

然后是返回值对象。

package com.example.gametrack.model; public class MonsterStatusVO { private Long monsterId; private String monsterName; private boolean captured; public Long getMonsterId() { return monsterId; } public void setMonsterId(Long monsterId) { this.monsterId = monsterId; } public String getMonsterName() { return monsterName; } public void setMonsterName(String monsterName) { this.monsterName = monsterName; } public boolean isCaptured() { return captured; } public void setCaptured(boolean captured) { this.captured = captured; } }

接着是 Mapper 接口。

package com.example.gametrack.mapper; import com.example.gametrack.model.MonsterStatusVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface MonsterMapper { List<MonsterStatusVO> listMonsterStatus(@Param("userId") Long userId, @Param("levelId") Long levelId); int insertCapture(@Param("userId") Long userId, @Param("monsterId") Long monsterId); }

Mapper XML 文件路径:src/main/resources/mapper/MonsterMapper.xml

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.gametrack.mapper.MonsterMapper"> <select id="listMonsterStatus" resultType="com.example.gametrack.model.MonsterStatusVO"> SELECT m.id AS monsterId, m.name AS monsterName, CASE WHEN uc.id IS NULL THEN 0 ELSE 1 END AS captured FROM monster m LEFT JOIN user_capture uc ON uc.monster_id = m.id AND uc.user_id = #{userId} WHERE m.level_id = #{levelId} ORDER BY m.id </select> <insert id="insertCapture"> INSERT INTO user_capture(user_id, monster_id, captured_at) VALUES (#{userId}, #{monsterId}, NOW()) ON DUPLICATE KEY UPDATE captured_at = captured_at </insert> </mapper>

然后是 Service 层。

package com.example.gametrack.service; import com.example.gametrack.mapper.MonsterMapper; import com.example.gametrack.model.MonsterStatusVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; @Service public class MonsterCaptureService { @Autowired private MonsterMapper monsterMapper; public List<MonsterStatusVO> listMonsterStatus(Long userId, Long levelId) { if (userId == null || levelId == null) { return Collections.emptyList(); } return monsterMapper.listMonsterStatus(userId, levelId); } public boolean capture(Long userId, Long monsterId) { if (userId == null || monsterId == null) { return false; } return monsterMapper.insertCapture(userId, monsterId) > 0; } }

最后是 Controller。

package com.example.gametrack.controller; import com.example.gametrack.model.MonsterStatusVO; import com.example.gametrack.service.MonsterCaptureService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/level") public class MonsterController { @Autowired private MonsterCaptureService monsterCaptureService; @GetMapping("/{levelId}/monsters") public List<MonsterStatusVO> listMonsters(@RequestParam Long userId, @PathVariable Long levelId) { return monsterCaptureService.listMonsterStatus(userId, levelId); } @PostMapping("/monster/{monsterId}/capture") public Map<String, Object> capture(@RequestParam Long userId, @PathVariable Long monsterId) { boolean success = monsterCaptureService.capture(userId, monsterId); Map<String, Object> result = new HashMap<>(); result.put("success", success); return result; } }

4.4 配置文件

文件路径:src/main/resources/application.properties

spring.application.name=gametrack server.port=8080 spring.datasource.url=jdbc:mysql://localhost:3306/game_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=你的数据库密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis.mapper-locations=classpath:mapper/*.xml

4.5 前端演示页面

为了快速验证功能,我写了一个原生 HTML 页面,放在src/main/resources/static/index.html下。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>关卡怪物捕获状态提示</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 800px; margin: 40px auto; padding: 0 16px; } .toolbar { display: flex; gap: 8px; margin-bottom: 16px; } input { width: 140px; padding: 6px 10px; border: 1px solid #d9d9d9; border-radius: 4px; } button { padding: 6px 14px; border: 1px solid #1890ff; background: #1890ff; color: #fff; border-radius: 4px; cursor: pointer; } .monster-card { display: inline-block; margin: 8px; padding: 16px; border-radius: 8px; background: #f7f7f7; min-width: 120px; text-align: center; } .captured { border: 1px solid #52c41a; background: #f6ffed; } .not-captured { border: 1px solid #d9d9d9; color: #888; background: #fafafa; } .badge { font-size: 12px; margin-top: 8px; } </style> </head> <body> <div id="app"> <h3>关卡怪物状态查询</h3> <div class="toolbar"> <input id="userId" placeholder="玩家ID" value="10001" /> <input id="levelId" placeholder="关卡ID" value="1" /> <button onclick="loadData()">查询</button> </div> <div id="result"></div> </div> <script> async function loadData() { const userId = document.getElementById('userId').value; const levelId = document.getElementById('levelId').value; const res = await fetch(`/api/level/${levelId}/monsters?userId=${userId}`); const data = await res.json(); render(data); } function render(list) { const result = document.getElementById('result'); result.innerHTML = ''; list.forEach(item => { const div = document.createElement('div'); div.className = 'monster-card ' + (item.captured ? 'captured' : 'not-captured'); const name = document.createElement('div'); name.textContent = item.monsterName; const badge = document.createElement('div'); badge.className = 'badge'; badge.textContent = item.captured ? '已捕获' : '未捕获'; div.appendChild(name); div.appendChild(badge); result.appendChild(div); }); } </script> </body> </html>

页面会调用后端查询接口,把每一个怪物渲染成一张卡片。已捕获的怪物显示绿色边框和“已捕获”文字,未捕获的怪物显示灰色边框和“未捕获”文字。

4.6 运行与验证

  1. 执行sql/init.sql初始化数据库。
  2. 修改application.properties中的数据库账号密码。
  3. 启动GameTrackApplication
  4. 浏览器访问http://localhost:8080
  5. 在页面输入玩家 ID 为10001,关卡 ID 为2,点击查询。
  6. 此时数据库里还没有玩家 10001 的捕获记录,所以两只怪物都应该显示“未捕获”。

然后手动调用一下捕获接口:

curl -X POST "http://localhost:8080/api/level/monster/4/capture?userId=10001"

这个接口会让玩家 10001 捕获怪物 ID 为 4 的幽灵。再次刷新页面,查询关卡 ID 为 2 时,幽灵显示“已捕获”,骷髅显示“未捕获”。

这里就实现了“提前知道这关的怪你有没有抓到过”的核心体验。

5. 常见问题与排查思路

问题现象常见原因排查思路与解决方式
查询结果里所有怪物都显示“未捕获”LEFT JOIN 的关联条件写错,或者把user_id条件放到了 WHERE 里检查 ON 条件是否同时包含monster_iduser_id;不要用 WHERE 过滤左表的 NULL 数据
玩家重复捕获后出现了多条捕获记录没有建立唯一索引,或唯一索引字段不对确认user_capture表存在UNIQUE KEY uk_user_monster (user_id, monster_id)
捕获接口返回成功,但查询还是未捕获先插入捕获记录,再查询时事务没有提交检查 Service 方法是否有事务控制;如果是自测,直接查数据库看记录是否存在
前端展示的还是旧状态浏览器缓存了 GET 接口结果查询接口可以加Cache-Control: no-cache,或在前端请求 URL 上追加时间戳参数
关卡内怪物数量多,查询变慢没有依赖索引,或者没有做缓存执行EXPLAIN查看 SQL 执行计划;确认monster.level_iduser_capture.user_iduser_capture.monster_id都有索引
未捕获的怪物名称被接口完整返回,玩家可以直接猜出来接口数据结构包含 name 字段如果产品不希望提前暴露怪物信息,后端需要根据captured字段决定是否返回monsterName,未捕获时显示占位符

排查时可以按照“数据是否正确写入 → SQL 是否能查出结果 → 接口返回是否正常 → 前端展示是否正确”的顺序逐层定位,能节省大量时间。

6. 最佳实践与工程建议

6.1 以服务端记录为准

玩家捕获状态的最终依据,必须是服务端数据库里的记录。客户端本地缓存可以用于临时加速展示,但不能作为全量数据源。否则玩家换设备、清缓存、重装后,状态就会丢失。

6.2 捕获接口要做幂等

捕获动作天然适合幂等设计。前后端都需要考虑网络重试:

  • 前端:捕获按钮在请求返回前置为禁用状态。
  • 后端:依靠唯一索引和ON DUPLICATE KEY UPDATE做兜底。

6.3 校验玩家权限

玩家只能操作自己的捕获记录。如果接口直接暴露userId参数,生产环境就必须校验当前登录用户是否等于传入的userId,避免越权。

安全边界的问题是很多接口最容易忽视的。开发时可以便捷,上线前要把这些风险标记清楚。

6.4 缓存策略要分级

如果一份怪物状态被大量玩家同时查询,数据库压力会很大。

比较常见的优化思路是:

  • 怪物和关卡的基础关系属于低频变化数据,可以缓存怪物的全部 ID 和名称。
  • 玩家的捕获记录更适合用 Redis 存储一个集合,比如capture:set:{userId},集合里存放该玩家已捕获的怪物 ID。
  • 查询时,直接通过集合判断怪物 ID 是否在集合内。

但缓存不是越复杂越好。如果项目初期玩家量不大,先做单表查询,完成需求后再引入缓存也不迟。

6.5 埋点与日志

建议在捕获成功的位置加日志,记录玩家 ID、怪物 ID、捕获时间、来源渠道。日志可以帮助后续排查问题,也能为游戏运营提供数据支撑。

对于查询接口,不建议每次查询都打印全量日志,避免日志量过大。

6.6 兼容历史数据

如果是给已有系统加这个功能,通常还要考虑旧数据问题。比如以前玩家已经捕获过怪物,但没有捕获记录,这时需要从历史战斗记录中回流数据,或者做一个补偿程序,把已有数据补写进user_capture表。

处理这类数据时,一定要先备份,再做幂等补偿,不能在未确认数据规则的情况下直接批量修改。

6.7 状态字段的可读性

不要把captured字段和“今天抓到过几次”“最近一次捕获时间”混在一个地方。职责单一的表结构,后续维护会轻松很多。

7. 总结与下一步学习方向

这个“提前知道本关怪物是否已捕获”的功能,本质上是一个很典型的用户状态查询需求。只要理解了怪物表、玩家捕获表、左连接匹配三件事,代码实现反而很简单。

如果其他项目要复用,我建议先画清楚两个问题:

  1. 怪物和关卡的关系是简单的一对多,还是更复杂的多对多。
  2. 捕获状态是否有过期或回退逻辑。

这两个问题确定了,数据库表结构和查询方案基本就能定下来。

下一步如果想深入优化,可以优先学习这几个方向:

  • 给查询接口加 Redis 缓存,减少数据库压力。
  • 把玩家状态表从关系型数据库迁移到更适合集合操作的存储。
  • 做好接口安全和权限校验,防止越权调用。
  • 针对百万级玩家数据量,设计更合理的分表分库方案。

基本思路就是这样。你可以先按这套代码跑通一遍,再根据自己项目的真实数据结构调整表名、字段和查询条件。只要状态写入够幂等、查询条件不写错,这个功能落地就非常快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 8:13:48

基于Qt的BSDiff与QLZ增量更新补丁工具实践

简介&#xff1a;面向需在Qt应用中集成增量更新机制的开发者&#xff0c;这份实践项目将bsdiff的差异比较能力与qlz快速压缩结合&#xff0c;完整展示生成补丁包并应用的流程。压缩包共705个文件、约6.22MB&#xff0c;包括675个idx索引文件、多个cpp/h源码、o目标文件、bin二进…

作者头像 李华
网站建设 2026/9/8 8:13:34

Windows事件日志监控与自动告警:从查询到Webhook通知的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 8:10:51

AI科技风PPT模板.zip:从解压、密码恢复到改造实战指南

简介&#xff1a;这是一份面向科技从业者、产品经理与学术汇报人员的人工智能科技风PPT模板&#xff0c;专门用来解决科技议题演示中视觉平淡、抽象概念难以传达的痛点&#xff0c;适用于AI项目路演、技术方案宣讲、年度科技工作总结等场景。资源包内共4个文件&#xff0c;核心…

作者头像 李华
网站建设 2026/9/8 8:09:50

船舶导航系统抗干扰测试全流程:从干扰源到数据判读

干船舶导航这行的人&#xff0c;大概都有过这种经历&#xff1a;集装箱船靠港时&#xff0c;明明卫星图上显示船位在码头前沿&#xff0c;电子海图上的船却慢悠悠往桥吊底下滑&#xff1b;要么是航行到某段繁忙水道&#xff0c;GNSS接收机突然所有卫星同时掉线&#xff0c;几秒…

作者头像 李华
网站建设 2026/9/8 8:08:16

AI陪伴产品防沉迷系统设计与工程实现

如果有一天&#xff0c;你的产品群里突然出现一条用户留言&#xff1a;“好浓的家属感&#xff0c;谁来把 Jan 的防沉迷关一下”&#xff0c;请不要只把它当段子。这句话里至少藏着三件事&#xff1a;用户真的很喜欢你的 AI 助手&#xff0c;用户已经意识到自己使用过度&#x…

作者头像 李华
网站建设 2026/9/8 8:07:48

国产MCU跑TinyML:AT32F435部署正弦波模型实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华