折腾考勤系统这事儿,只有亲自从零到一收拾过的人才说得清坑在哪。先说结论:在 .NET 生态里完整对接一套飞书考勤,不是小工程,但把整条链路的环节摸透了,两三天也够落地一套稳定可跑的版本。核心就四块:飞书开放平台的权限申请、令牌与加密事件处理、按天拉取原始打卡记录落库、最后在自有服务里做工时规则计算和异常汇总。这篇内容没有照着官方文档念,全部来自实际踩坑后的整理,适合正在做企业内部系统集成、准备自建考勤中台的开发者参考。
这几年很多公司用上了飞书考勤,后台看报表很方便,但一到月底要导出数据、跟现有HR系统或薪酬系统联动,就会明显感觉原始能力不够用。飞书开放平台提供了考勤相关的 OpenAPI,但数据怎么拉、怎么存、怎么跟内部排班规则对上,完全没有现成答案。而我这边当时接到需求是要把飞书打卡数据、请假审批结果统一汇总到内部薪资系统,避免人工导出再录入。这个项目没有太复杂的算法,更多的是工程化落地:把 API 封装好、定时任务铺好、异常数据筛查清楚、回调事件接稳,整个系统就算立住了。
1. 项目整体设计与选型思路
1.1 核心需求拆解
这个项目的真实需求并不只是“把飞书考勤数据同步过来”,而是建设一条从飞书到内部 HR 系统的数据通路。具体拆解下来需要覆盖几个能力:员工打卡记录的自动采集,请休假及审批结果的回流,考勤异常如迟到、早退、缺卡的自动判别,以及同步过程和结果的留档审计。飞书后台虽然自带日报、月报,但数据下钻和二次计算能力有限,尤其是多考勤组混合排班时,自定义规则根本没法在飞书后台实现。
我当时梳理出的核心逻辑是:飞书作为数据源,只负责把考勤原始数据吐出来,自定义的考勤规则全部放到自己的服务里计算。这样飞书版本升级或考勤配置变化时,只需要调整适配层,业务规则永远在自己可控的代码里。
1.2 技术选型为什么落在 .NET
团队当时技术栈以 .NET 为主,现有的薪酬、人事模块都是 ASP.NET Core 服务,所以在中间集成层继续使用 .NET 是最省事的选择。不过这里有个很现实的问题:飞书官方没有 .NET SDK,官方主推的是 Java、Go、Python 和 Node.js。这就意味着所有 API 调用、事件回调、加密解密都要自己用 HttpClient 封装,工作量和踩坑概率都会增加。
但换个角度看,这反而是个优势。不依赖 SDK 的话,核心请求只是 HTTP POST/GET,什么语言都能接。把飞书开放平台认证、签名、回调验证的细节摸清楚,这层封装在后面很多飞书扩展需求中都可以复用。当时我们的做法是单独建了一个基础通信库,考勤只是它服务的第一个业务模块。
1.3 项目模块划分
整个项目我拆成了四个子模块,职责非常清晰:
- API 封装层:处理应用令牌获取、刷新、API 路径管理、请求响应统一解析。这一层不关心考勤业务,只做通用通信。
- 数据同步层:包含定时任务和实时回调两条链路。定时任务负责拉取前一天或指定时间段的打卡数据,回调负责接收飞书推送的考勤记录变更事件。
- 业务计算层:将拉取到的原始打卡记录与排班规则做匹配,算出正常出勤、迟到、早退、缺卡、加班等情况,并将请假数据合并进来。
- 数据落库层:负责将计算结果写入数据库,同时维护同步日志与异常记录。
这样做的好处是任何一个模块出问题都能独立排查,不会因为业务逻辑变动而连带底层通信代码一起改。
2. 前置准备:应用申请与关键配置
2.1 飞书自建应用与权限开通
飞书开放平台上创建应用时,一定要选择自建应用,而不是商店应用。自建应用属于企业内部的私有集成,可以自由配置权限范围和回调事件,商店应用则要过审核、走分发流程,明显不适合内部系统对接。
创建完成后,最关键的步骤是开通权限。考勤相关的接口至少需要以下权限:
| 权限标识 | 用途 |
|---|---|
| attendance:record:read | 读取打卡记录 |
| attendance:group:read | 读取考勤组信息 |
| attendance:user_task:read | 读取用户考勤任务 |
| attendance:day_off:read | 读取请假信息 |
| approval:instance:read | 读取审批实例 |
| contact:user.base:read | 读取用户基础信息 |
这里非常容易踩的坑是只开通部分权限,结果接口返回 99991663 之类的权限错误。开通权限后必须发布应用版本,飞书开放平台上的权限不是开通即时生效的,需要创建版本并发布,发布后还要等一两分钟缓存才刷新。
2.2 事件订阅与回调地址配置
事件回调是这套系统的实时性保障,飞书考勤有记录变更事件,审批流程也有实例事件。在应用后台的事件订阅配置中,需要把回调地址填成自己服务的 Webhook 地址。
配置回调地址时有两个关键参数:Encrypt Key 和 Verification Token。Encrypt Key 用于回调内容加密,这个建议必须开。不开加密的话,回调内容明文传输,员工考勤信息这种隐私数据裸奔在公网上无论如何都说不过去。开启后每一次回调推送都会带上一个加密字段,服务端需要用 Encrypt Key 做解密。
回调地址填好后飞书会发送一个验证请求,需要服务端正确响应 Challenge 值才能配置成功。这个验证逻辑要提前写进代码里。
2.3 .NET 项目基础搭建
项目从一个大而全的 ASP.NET Core Web API 起步就行,不需要拆得太细。当时我创建的是 .NET 8,使用 HostedService 容纳定时同步逻辑,同时开放一个 Webhook 控制器接收飞书回调。
用到的 NuGet 包就几个:
Microsoft.EntityFrameworkCore.Sqlite # 本地交付用数据库 Microsoft.EntityFrameworkCore.Design System.Text.Json前期的调用直接按普通 JSON 处理,不引入太重的东西。核心配置用环境变量注入,避免把 App Secret 写死在代码里,后面部署和交接时安全性会好很多。
3. 核心封装:Token 管理与 API 请求
3.1 租户访问令牌缓存与刷新
飞书开放平台接口绝大多数都要带tenant_access_token,它是应用级别的访问令牌,不是用户维度。获取方式是一名内网应用凭 App ID 和 App Secret 换令牌,官方推荐有效期约 2 小时,到期后需要重新申请。
这里最容易出现的错误是每次请求前都现拿 Token,既浪费请求,又可能出现并发下令牌刷新竞争。我的做法是做一个单例的 TokenService,内部用内存缓存维护当前令牌,并记录过期时间。
public class TenantTokenService { private readonly HttpClient _httpClient; private readonly IConfiguration _config; private string? _accessToken; private DateTime _expireAt; public TenantTokenService(HttpClient httpClient, IConfiguration config) { _httpClient = httpClient; _config = config; } public async Task<string> GetTokenAsync() { if (_accessToken != null && _expireAt > DateTime.UtcNow.AddMinutes(5)) { return _accessToken; } var appId = _config["Feishu:AppId"]; var appSecret = _config["Feishu:AppSecret"]; var payload = new { app_id = appId, app_secret = appSecret }; using var resp = await _httpClient.PostAsJsonAsync( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", payload); var body = await resp.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(body); var root = doc.RootElement; if (root.GetProperty("code").GetInt32() != 0) { throw new InvalidOperationException($"获取令牌失败: {root.GetProperty("msg").GetString()}"); } _accessToken = root.GetProperty("tenant_access_token").GetString(); var expire = root.GetProperty("expire").GetInt32(); _expireAt = DateTime.UtcNow.AddSeconds(expire); return _accessToken!; } }提前五分钟刷新是一个细节,避免令牌刚好在请求过程中过期导致的鉴权失败。飞书接口层或网关层偶尔有延迟,留出缓冲时间实际效果好很多。
3.2 考勤任务查询 API 对接
飞书考勤数据最核心的接口是用户考勤任务查询,拉取到的是某个用户在某段时间内的每日考勤小结,里面包含考勤组、打卡记录、请假记录等。这个接口返回的数据结构比较深,封装时建议直接用强类型模型映射,不要每次用 JsonDocument 动态解析。
public class UserTaskQueryRequest { public List<string> user_ids { get; set; } = new(); public long work_from_ts { get; set; } public long work_to_ts { get; set; } public bool need_day_off_records { get; set; } public string user_id_type { get; set; } = "open_id"; } public class CheckRecordDto { public long check_time { get; set; } public string io_state { get; set; } // I/O public string method { get; set; } public double? latitude { get; set; } public double? longitude { get; set; } } public class UserTaskDto { public string user_id { get; set; } public long date { get; set; } public List<UserTaskGroupDto> groups { get; set; } = new(); }对应查询请求需要调用POST /open-apis/attendance/v1/user_tasks/query接口。查询时要传入 Unix 秒级时间戳,很多业务系统习惯用毫秒级,这里直接照搬飞书要求就行。
我当时封装了一个查询方法:
public async Task<List<UserTaskDto>> QueryUserTasksAsync( string accessToken, List<string> userIds, DateTime from, DateTime to, CancellationToken ct = default) { var reqBody = new UserTaskQueryRequest { user_ids = userIds, work_from_ts = new DateTimeOffset(from.ToUniversalTime()).ToUnixTimeSeconds(), work_to_ts = new DateTimeOffset(to.ToUniversalTime()).ToUnixTimeSeconds(), need_day_off_records = true }; using var httpReq = new HttpRequestMessage(HttpMethod.Post, "https://open.feishu.cn/open-apis/attendance/v1/user_tasks/query"); httpReq.Headers.TryAddWithoutValidation("Authorization", $"Bearer {accessToken}"); httpReq.Content = JsonContent.Create(reqBody); using var resp = await _httpClient.SendAsync(httpReq, ct); var body = await resp.Content.ReadAsStringAsync(ct); // 按 code 判断响应是否成功 using var doc = JsonDocument.Parse(body); var root = doc.RootElement; if (root.GetProperty("code").GetInt32() != 0) { throw new FeishuApiException(root.GetProperty("code").GetInt32(), root.GetProperty("msg").GetString()); } var tasksJson = root.GetProperty("data").GetProperty("user_tasks"); return tasksJson.Deserialize<List<UserTaskDto>>(JsonOptions.Value) ?? new(); }有个细节要提醒:查询时间范围有长度限制,时间跨度超过一个月建议拆成多次查询。我当时是按天循环查询,每次只拉一天的数据,虽然请求量多了点,但稳定性和问题排查难度都友好很多。
3.3 响应结构与错误码处理
飞书所有 OpenAPI 的响应结构是统一的,最外层有code、msg和data三个字段。特别需要注意的是,HTTP 状态码正常返回 200,不代表业务就成功,业务错误全都在code字段里体现。
常见的几个错误码我整理成了速查表:
| code | 含义 | 处理建议 |
|---|---|---|
| 99991661 | 权限不足 | 检查应用权限并重新发布版本 |
| 99991663 | 接口不存在或不可用 | 检查接口路径与版本 |
| 99991664 | 获取访问令牌失败 | 检查 AppId/AppSecret |
| 99991668 | 访问频率超限 | 增加退避重试,降低调用频率 |
| 99991672 | 令牌失效 | 清理缓存并重新获取 |
在封装公共层时要将code和msg包装到自定义异常里,流水日志里同时记录请求体、响应体和耗时。出错时排查问题会非常省事。
4. 数据同步链路与定时任务
4.1 数据库表设计
同步链路的核心是两张表:原始打卡记录表和每日考勤汇总表。原始记录表忠实地保存从飞书拉下来的数据,这张表是追溯用的,不允许任何业务修改;每日汇总表则存经过计算后的结果,供薪酬和报表模块查询。
建表脚本如下:
CREATE TABLE attendance_raw_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, check_time DATETIME NOT NULL, io_state TEXT NOT NULL, method TEXT, group_id TEXT, group_name TEXT, location_name TEXT, raw_json TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance_daily_summary ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, work_date TEXT NOT NULL, should_hours REAL, actual_minutes INTEGER, late_minutes INTEGER, early_leave_minutes INTEGER, absent INTEGER DEFAULT 0, unpaid OVERTIME INTEGER DEFAULT 0, day_off_type TEXT, status TEXT, UNIQUE(user_id, work_date) );数据库最初用的是 SQLite,一人一机或中小团队环境完全够用。后面要从内网服务升级成多实例部署,再把数据库切到 PostgreSQL 也比较简单,因为所有 SQL 都集中在 EF Core 仓储层。
4.2 后台定时同步实现
同步用BackgroundService做了个简单的定时循环,每两小时触发一次。由于考勤记录是当天实时产生的,做实时同步没有很大意义,最终以凌晨把前一天的完整数据拉齐为主。
Worker 的核心逻辑是:查数据库里有哪些员工需要同步,按用户维度循环调用飞书接口,拿到结果后先写入原始记录表,再做汇总计算。
public class AttendanceSyncWorker : BackgroundService { private readonly TenantTokenService _tokenService; private readonly AttendanceApiService _apiService; private readonly AttendanceCalculator _calculator; private readonly IServiceScopeFactory _scopeFactory; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await SyncYesterdayAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "考勤同步任务异常"); } await Task.Delay(TimeSpan.FromHours(2), stoppingToken); } } }为了避免某个用户的数据导致整个批次失败,同步循环里要对单用户做 try-catch。拉取失败的记入同步失败表,下一轮自动重试,连续失败超过三次再人工介入。
4.3 open_id、user_id 与 employee_id 的映射关系
这是集成飞书时最绕的一个点。飞书接口可能要求传用户ID,返回的用户标识字段名却又有可能是另一个体系。比如user_tasks/query接口返回的user_id字段,实际到底对应 open_id 还是 employee_id,取决于请求参数里的user_id_type。
我项目里的统一口径是:内部系统全部用open_id作为唯一标识。员工开通飞书后,调用一次通讯录接口把open_id和内部员工工号建立映射表,之后所有查询都走 open_id,绝不混用。
如果你拿到的数据源是花名册系统导出的员工编号,一定得先确认那是什么类型的ID。因为 open_id 是一个应用维度下的唯一标识,同一个用户在 A 应用和 B 应用下的 open_id 是不同的,不能直接拿 A 应用的 open_id 去另一个应用换数据。
5. 回调事件与审批联动
5.1 回调验签与数据解密
飞书事件回调开启加密后,请求体只有encrypt字段,里面是用 Encrypt Key 加密后的业务 JSON。解密算法是 AES-256-GCM,不少人在这一步翻车。我自己第一次对接时也拼错过 nonce 和 tag 的位置,解密一直报错,后来把二进制输出的十六进制对了一遍才定位问题。
下面是可行的解密方法,基于 .NET 内置的 AES-GCM:
public static string FeishuEventDecrypt(string encrypt, string encryptKey) { var payload = Convert.FromBase64String(encrypt); var nonce = payload.AsSpan(0, 12); // 前 12 字节是 nonce var cipherAndTag = payload.AsSpan(12); // 剩余是密文 + 16 字节 tag var tag = cipherAndTag.Slice(cipherAndTag.Length - 16); var cipherBytes = cipherAndTag.Slice(0, cipherAndTag.Length - 16); using var aes = new AesGcm(Convert.FromBase64String(encryptKey), 16); var plainBytes = new byte[cipherBytes.Length]; aes.Decrypt(nonce, cipherBytes, tag, plainBytes); return Encoding.UTF8.GetString(plainBytes); }注意这里的 Encrypt Key 不是直接当字符串用,而是一个 Base64 编码的 32 字节密钥,先解码再传入算法。解密成功后的 JSON,通常会包含type、challenge或event字段,再按事件类型分发处理。
5.2 考勤记录变更事件
飞书有考勤记录变化的事件,员工完成打卡后,平台会向回调地址推送事件。考虑到定时同步的延迟和异常,这个实时事件的价值在于可以立刻感知异常打卡,比如员工在非打卡点打卡,或者重复打卡。
事件结构一般是 schema 2.0 的格式,外层有header.event_type,里面才是event数据体。我在控制器里按 event_type 做了个简单分发器:
[HttpPost("webhook/feishu")] public async Task<IActionResult> HandleFeishuEvent(JsonElement body) { // 解密与验签逻辑略 var eventType = body.GetProperty("header").GetProperty("event_type").GetString(); switch (eventType) { case "attendance.record_change": await HandleAttendanceRecordChangeAsync(body.GetProperty("event")); break; case "approval.instance.v2": await HandleApprovalInstanceAsync(body.GetProperty("event")); break; } return Ok(new { code = 0 }); }回调处理一定不要做耗时操作,飞书对响应时间有限制。建议在 Worker 里做异步处理,接口先快速返回 HTTP 200,把事件内容放入内存队列或数据库队列表,后台再消化。
5.3 审批实例事件与请假数据合并
请假数据并不在打卡记录接口里,而是走审批流。飞书审批实例事件推送内容包括审批类型、发起人、通过状态等。真正到月底算工资时,不仅要看有没有打卡,还要看有没有请假单。
我的处理方式是监听approval.instance.v2事件,当审批状态变为通过时,将审批信息落库。随后在每日考勤汇总计算时,如果当天存在已通过的请假单,就将当天标记为请假,不再做迟到缺卡判定。
有一个业务细节得说:审批通过事件可能滞后,如果员工在月底补交请假单,会导致之前已经算好的汇总数据变化。为此汇总记录里加了一个last_calc_version字段,每次重新计算时直接以最新审批结果覆盖,不保留历史中间态。
6. 工时计算与异常判定规则
6.1 考勤组规则建模
每个考勤组的班次规则、休息时间、上班时间都不同。飞书接口返回的time_rule字段包含规则明细,但不同企业排班逻辑差异很大,不宜直接拿默认规则套用。我设计的方案是把规则映射表做成可配置项,后台可以维护。
一条规则包含:规则名、上班时间、下班时间、迟到容忍分钟数、早退容忍分钟数、是否弹性打卡。用一张attendance_rule表维护。
计算公式是:
实际出勤分钟数 = 下班打卡时间 - 上班打卡时间 - 休息分钟数 迟到分钟数 = 上班打卡时间超过规定上班时间的部分 早退分钟数 = 下班打卡时间早于规定下班时间的部分展开说一个容易忽略的细节,考勤系统里上班打卡和下班打卡分别对应io_state为 I 和 O。同一天可能有多条打卡记录,正常上班一次、下班一次,有人中午外出还会打多次卡。汇总逻辑里每个方向取最合理的那条,比如上班卡取最早一条,下班卡取最晚一条。这看起来简单,却有业务依据:大多数公司执行的规则就是上班卡按最早,下班卡按最晚。
6.2 异常状态自动分类
汇总完成后,将每个员工每天的状态分成几类:
| 状态 | 判定条件 |
|---|---|
| 正常 | 有上下班打卡且实际工时达标 |
| 迟到 | 上班打卡时间晚于规则时间且超容忍值 |
| 早退 | 下班打卡时间早于规则时间且超容忍值 |
| 缺卡 | 缺少上班或下班任一打卡 |
| 请假 | 当天有已通过的请假审批 |
| 旷工 | 当天无打卡且无请假 |
状态判定代码不复杂,但边界条件要细致。比如法定节假日或调休,这类日期需要先标记到节假日表里,汇总计算遇到节假日直接跳过,不产生旷工结果。
6.3 工时报表输出
计算结果最后落到attendance_daily_summary,同时支持按月导出。薪酬系统对接时直接查询汇总表,不需要逐条核对原始打卡记录。只有薪酬系统对数据有疑问时,才人工翻查原始记录表。
这个输出阶段有一个值得注意的问题:时区。飞书的date字段以及打卡时间戳都是 Unix 秒,默认按零时区解析。业务侧展示时必须转成北京时间,否则每天的时间都偏移八小时。我当时在查询层就直接统一转成了 DateTimeOffset 类型,后续业务不会再因为时区问题重复踩坑。
7. 部署细节与常见问题排查
7.1 部署形态与环境变量管理
部署上我选了最简单的方案:单体服务加 SQLite,打一个 Linux 容器发布到内网服务器。.NET 8 镜像本来就比较小,发布后的容器只有一百兆左右,即使跑在低配服务器上也毫不吃力。
环境变量统一从配置中心注入,测试环境和正式环境使用不同的 AppId 和 AppSecret。最忌讳的是把正式环境的 Secret 写到 appsettings.json 里提交到代码仓库,一旦泄露,别人的应用就能伪装你的服务身份调用飞书接口。
建议在发布脚本里用如下环境变量:
export FEISHU_APP_ID="cli_xxx" export FEISHU_APP_SECRET="your_secret_here" export FEISHU_ENCRYPT_KEY="your_encrypt_key"代码里用IConfiguration统一读取,开发环境从环境变量取值,生产环境走配置中心。
7.2 高频踩坑与解决实录
以下问题的优先级从高到低排序,一旦遇到先看这一节:
请求 99991668 频率超限。飞书开放平台对每个应用的接口调用频率有限制。实际场景里如果全公司几百人一起拉数据,很容易把配额打爆。解决方法应该是增加本地缓存,不要对同一用户重复拉取同一时间段的数据,同时在代码里做线程安全控制,防止定时任务和手动触发的同步任务并发执行。
首次同步时公司历史数据量大导致超时。飞书接口单次查询支持批量用户,但批量太大容易超时。我实测下来每次查询 20 个用户比较稳,配合分页处理,一万人的企业也就几百个请求,均匀分布在一个小时内完全没问题。
回调验签报错。绝大多数开发者在对接飞书回调时出现验签失败,都是因为加了自签证书或者本地代理,中间人改写请求导致签名不匹配。测试阶段可以先把验签临时跳过,单测跑通业务,再补上验签逻辑。真要线上验签失败,优先检查服务器时间,飞书签名校验依赖时间戳,时钟漂移同样会导致失败。
同步任务静默失效。定时任务最怕的不是报错,而是什么也不做。日志系统里一定要记录每次同步的起始时间、拉取员工数量、成功数和失败数。我养成的习惯是每天凌晨同步结束后给运维邮箱发一封汇总邮件,因为如果第二天早上汇总数据异常,及时发现成本远比月底对账低得多。
7.3 性能与稳定性优化建议
这套系统的数据量级撑住一万人的日粒度同步完全没问题,但考虑长期运行还是要做两个优化。
第一,HttpClient 一定要通过依赖注入复用,不要每次 new。连接池复用对飞书接口的长连接开销优化非常明显,服务器连接数从几百降到个位数。
第二,同步队列化。员工数量多时,把用户 ID 分批丢进 Channel,多个并发消费者从 Channel 取批次请求。这种生产消费模型比粗暴的 Task.WhenAll 更可控,限流和重试都更容易实现。
数据库层的索引也不可忽视。attendance_raw_record表要按 user_id 和 check_time 建联合索引,否则一个月的数据量查起来就开始明显变慢。SQLite 的话记得开 WAL 模式,读写并发会有明显提升。
8. 关键设计经验沉淀
项目做完后回看,最有价值的不是这段代码本身,而是几个贯穿始终的工程判断。
任何与外部平台对接的系统,必须把第三方接口的不确定性隔离在一个独立适配层里。飞书考勤的数据结构从第一次对接到现在迭代过几次,如果业务代码里到处散落着飞书字段,任何一次接口调整都够团队喝一壶。适配层里做字段映射和版本兼容,内部业务代码永远用自有的模型,才能保证核心逻辑的稳定性。
定时任务一定要有失败可观察性。我看过太多数据同步服务,出问题时不报错、只是不写数据,业务人员月底拿着报表找过来才知道系统已经默默坏了两周。同步日志、结果统计、失败重试、超时告警,这些看起来不产生业务价值的东西,恰恰是这类系统的生命线。
对照飞书开放平台做集成时,“能跑通”只是第一关,能持续稳定地每天跑、每月对账不出错,才是真正落地的标准。当时项目交付后,我把整个代码库里飞书回调里所有字段的文档链接、权限申请记录、常见错误码处理方式都整理成了一份交付文档。后面的维护人员接手系统时不用重新趟坑,这一点我至今觉得做得最值。
最后分享一个小教训:即使飞书开放平台自带了测试工具,也请务必用一个独立的测试企业租户做联调,别在正式企业里反复打测试数据。考勤数据是会进工资计算的,任何一次脏数据注入都可能变成月底薪酬对账的噩梦。