一、保存触发——什么时候开始保存
保存动作的触发点有两个:消息回调触发(实时收到图片或文件时立刻保存)和定时补录触发(扫描历史消息,补存遗漏的素材)。
回调触发是主力,但回调可能丢(5秒超时、服务重启、网络波动),所以定时补录是兜底。两种触发配合,保证素材不遗漏。
二、保存路径——存到哪里怎么存
素材存储有三层选择:临时目录(下载后快速暂存,定期清理)、本地持久目录(按日期分文件夹永久保存)、对象存储(OSS/S3,可扩展可备份)。
建议方案:小量素材存本地目录按日期分文件夹(简单够用),量大后迁移到对象存储。路径规则统一:/{year}/{month}/{day}/{msgId}.{ext},按日期可浏览,按 msgId 可检索。
三、保存索引——怎么找到存了的素材
素材存下来不等于找得到。每条素材都要建索引记录:消息ID、发送人、群ID、文件名、文件类型、存储路径、文件大小、接收时间、处理状态。
索引建在数据库里,查询时按发送人查、按时间范围查、按文件类型查、按关键词查都能做到。没有索引的素材等于丢了——文件在硬盘上但没人知道在哪。
四、保存完整性——怎么确保存对了
保存完整性三个检查点:下载后校验文件大小(和回调里的对比)、校验文件类型(扩展名和实际文件头对比)、校验文件可读性(图片能否打开、PDF能否解析)。
校验失败的标记为"损坏",保留原始下载内容待修复,不直接删除。定期扫描损坏文件列表,人工排查原因。
保存策略对照
保存环节 | 做什么 | 关键设计 |
|---|---|---|
触发 | 什么时候保存 | 回调实时 + 定时补录 |
路径 | 存哪里怎么存 | 本地按日期分目录 / 对象存储 |
索引 | 怎么找存了的 | 数据库索引:msgId/发送人/类型/路径 |
完整性 | 怎么确保存对了 | 大小校验、类型校验、可读性校验 |
素材保存完整流程
import os, hashlib def save_media(msg_id, ref, from_user, file_name, msg_type): # 下载 raw = download_with_retry(ref) if not raw: db.log_media(msg_id, status="download_failed") return # 路径:按日期分目录 today = datetime.now().strftime("%Y/%m/%d") ext = file_name.rsplit(".", 1)[-1].lower() if "." in file_name else "dat" path = f"/media/{today}/{msg_id}.{ext}" os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(raw) # 索引:落库 db.save_media({ "msg_id": msg_id, "from_user": from_user, "file_name": file_name, "file_type": ext, "path": path, "size": len(raw), "status": "saved", "create_time": datetime.now() }) # 完整性校验 if not verify_file(path, ext): db.update_media(msg_id, status="corrupted")落地建议
素材保存的核心是"存得下来、找得到、不损坏"。回调触发和定时补录缺一不可——只靠回调迟早丢数据。路径规则从第一天就设计好,后期迁移成本极高。索引表必须建,没有索引的文件等于不存在。媒体下载和存储接口说明参考 Eyun 开发文档,实例开通见 Eyun 官网。