尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Listmonk 订阅者批量导入 API 完整指南:CSV/ZIP 上传、状态轮询与源码级实现解析

Listmonk 订阅者批量导入 API 完整指南:CSV/ZIP 上传、状态轮询与源码级实现解析 Listmonk 订阅者批量导入 API 完整指南CSV/ZIP 上传、状态轮询与源码级实现解析【免费下载链接】listmonkHigh performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.项目地址: https://gitcode.com/GitHub_Trending/li/listmonk导读本文是 listmonk 中订阅者批量导入Bulk Subscriber ImportAPI 的实战指南覆盖/api/import/subscribers系列四个端点上传、状态查询、日志查询、停止导入的请求格式、参数语义与典型调用方式。在 listmonk 中当需要将历史订阅者数据、第三方导出的 CSV 或跨系统迁移的用户列表一次性写入系统时这套 API 是唯一面向自动化场景的官方入口。读完本文你将掌握如何构造 multipart 上传请求、理解params中每个 JSON 字段含文档未展开的subscription_status、overwrite_userinfo、overwrite_subscription_status的实际效果以及导入器在后台如何处理 CSV、批量落库与状态反馈——并辅以仓库源码佐证每个结论。一、端点总览与权限说明listmonk 将导入功能封装为同一资源路径下的四个 REST 端点全部位于 API 分组中定义在 cmd/handlers.goMethodEndpointDescriptionGET/api/import/subscribers查询当前导入的状态与统计GET/api/import/subscribers/logs查询最近一次导入的日志POST/api/import/subscribers上传 CSV可选 ZIP 压缩文件并启动批量导入DELETE/api/import/subscribers停止正在进行的导入或清除已结束导入的状态从路由注册代码可见四个端点统一挂在subscribers:import权限permission之下均通过pm(...)中间件做认证与授权。这意味着调用方必须是具备该权限的用户通常以-u api_user:token形式的 HTTP Basic Auth 通过认证api_user为 listmonk 管理员用户名token为其 API token。在系统内部这四个端点分别对应 cmd/import.go 中的四个 HandlerImportSubscribersPOST校验参数 → 保存上传文件 → 启动导入会话GetImportSubscribersGET返回a.importer.GetStats()GetImportSubscriberStatsGET 日志返回a.importer.GetLogs()StopImportSubscribersDELETE调用a.importer.Stop()后返回当前统计。二、导入文件格式准备CSV 列、分隔符与 ZIP在发起请求前先理解 listmonk 期望的文件结构这决定了导入能否成功。CSV 列约定导入器只识别三个表头列定义于 internal/subimporter/importer.go 的csvHeaders映射列名是否必填说明email是订阅者邮箱缺失该列将直接报错email column not foundname否订阅者姓名留空时由系统根据邮箱用户名部分自动生成见下文attributes否JSON 字符串形式的订阅者自定义属性列顺序不固定。源码中的mapCSVHeaders会按表头名称动态建立“列名 → 列索引”的映射importer.go因此email,name,attributes与attributes,name,email都能正确解析。表头中的未知列会被忽略并写入日志ignoring unknown header不会导致导入失败。一个可用的示例文件该示例同时出现在管理后台导入页 frontend/src/views/Import.vue 中email,name,attributes user1mail.com,User One,{age: 42, planet: Mars} user2mail.com,User Two,{age: 24, job: Time Traveller}注意attributes列中的 JSON 使用双引号包裹转义后的 JSON 文本CSV 中表示一个字面双引号。分隔符delimiterdelim参数指定 CSV 文件使用的单字符分隔符常见为,。源码在 cmd/import.go 中强制校验len(opt.Delim) ! 1即返回import.invalidDelim错误所以制表符\t、分号;等单字符分隔符均可使用但必须是单个字符。ZIP 压缩包支持POST 端点接受普通.csv文件也接受将 CSV 打包后的 ZIP 文件。上传处理逻辑cmd/import.go文件名以.csv结尾不区分大小写→ 直接按 CSV 解析其他文件名 → 视为 ZIP调用sess.ExtractZIP(out.Name(), 1)解压。ZIP 处理有几个值得注意的规则实现于 importer.go只提取包内第一个.csv文件参与导入其余 CSV 文件会被忽略——因此若有多个 CSV官方建议先自行拼接为一个 CSV 再上传压缩包内的目录条目与非.csv文件会被跳过并记录日志文件名经filepath.Base清洗防止 ZIP slip 路径穿越安全考虑若包内没有任何 CSV 文件报错no CSV files found in the ZIP并使导入进入failed状态。三、POST /api/import/subscribers启动批量导入这是整个导入流程的入口。请求必须使用multipart/form-data表单包含两个字段NameTypeRequiredDescriptionparamsJSON stringYes字符串化的 JSON包含导入参数filefileYes待上传的 CSV或 ZIP文件3.1params完整参数表原文档给出的参数为mode、delim、lists、overwrite。结合 internal/subimporter/importer.go 中SessionOpt结构的json标签完整的参数集如下NameTypeRequiredDescriptionmodestringYessubscribe订阅或blocklist拉黑subscription_statusstring否导入后订阅者的订阅状态unconfirmed/confirmed/unsubscribed省略时按 mode 取默认值delimstringYesCSV 分隔符单字符如,lists[]number视模式要加入的列表 ID 数组subscribe模式下必填overwritebool否兼容旧版字段为true时等价于同时开启下面两个 overwrite 字段overwrite_userinfobool否已存在的订阅者是否用文件中的 name/attributes 覆盖其资料overwrite_subscription_statusbool否已存在的订阅者是否用文件中的状态覆盖其订阅状态overwrite与两个细粒度字段的关系在NewSession中有明确说明importer.go为了 API 向后兼容只要设置了旧的overwrite: trueoverwrite_userinfo与overwrite_subscription_status会被同时置为true。3.2 参数校验与默认值服务端行为从 cmd/import.go 可以看到 POST 处理器的完整校验链调用方值得了解这些约束以避免无谓的 400 错误并发限制若已有导入正在运行状态为importing直接返回400 import.alreadyRunning——同一时刻只允许一个导入会话列表权限过滤opt.ListIDs user.FilterListsByPerm(auth.PermTypeManage, opt.ListIDs)当前用户无权管理的列表 ID 会被过滤掉若过滤后为空且模式不是blocklist返回403权限拒绝mode 校验只允许subscribe或blocklist否则返回400 import.invalidModesubscription_status 默认值cmd/import.gosubscribe模式默认unconfirmedblocklist模式默认unsubscribed显式传入的值必须是unconfirmed/confirmed/unsubscribed三者之一否则报import.invalidSubStatusdelim 长度必须为 1file 字段必须存在且可打开否则返回import.invalidFile。上传的文件会被复制到系统临时目录os.CreateTemp(, listmonk)随后新建导入会话并异步启动处理go sess.Start() go sess.LoadCSV(out.Name(), rune(opt.Delim[0]))因此 POST 接口会立即返回真正的导入在后台 goroutine 中执行需要配合状态查询接口跟踪进度。3.3 标准调用示例curl -u api_user:token -X POST http://localhost:9000/api/import/subscribers \ -F params{mode:subscribe, subscription_status:confirmed, delim:,, lists:[1, 2], overwrite: true} \ -F file/path/to/subs.csv响应示例返回当前统计快照data字段结构与 GET 状态接口一致{ data: { name: subs.csv, total: 500, imported: 0, status: importing } }说明name为上传文件的原始文件名由opt.Filename file.Filename记录total为文件总数据行数不含表头imported为已成功落库的记录数status见下文状态机。四、GET /api/import/subscribers查询导入状态与进度4.1 请求与响应curl -u api_user:token -X GET http://localhost:9000/api/import/subscribers{ data: { name: , total: 0, imported: 0, status: none } }当没有进行过任何导入时返回name为空、status为none的初始状态Importer初始化时的默认状态见 importer.go。4.2 状态机与统计字段语义status字段的全部取值定义在 importer.go状态值含义none空闲无导入在运行也没有未清除的已完成会话importing导入进行中stopping收到停止信号正在排空剩余队列finished导入成功结束failed导入失败解析错误、事务提交失败等统计字段的精确语义LoadCSV 与 countLinestotal通过统计换行符得到的文件总行数再减 1排除表头行用作前端进度条的基数imported已成功写入数据库的记录数由incrementImportCount在每次批量提交时累加由于total是“行数”而非“有效记录数”被跳过的非法行会导致最终imported略小于total这是正常现象详见下文错误处理章节。五、GET /api/import/subscribers/logs查看导入日志5.1 请求与响应curl -u api_user:token -X GET http://localhost:9000/api/import/subscribers/logs{ data: 2020/04/08 21:55:20 processing import.csv\n2020/04/08 21:55:21 imported finished\n }5.2 日志内容说明日志由Session中的log.Logger写入importer.go带日期时间与源码位置前缀。管理后台前端在展示时会用正则去掉importer.go:行号:前缀见 Import.vue 的getLogs方法。典型的日志事件包括processing 文件名会话开始skipping line N. ...某行记录因校验失败或列数不匹配被跳过原因各不相同imported 累计数每完成一个批量提交输出一次imported finished全部处理完成stop request received收到停止信号extracting .../skipping non .csv file ...ZIP 解压过程的日志。日志缓冲区在会话开始时重建NewSession中logBuf: bytes.NewBuffer(nil)因此该接口返回的是最近一次导入会话的日志。六、DELETE /api/import/subscribers停止或清除导入6.1 请求与响应curl -u api_user:token -X DELETE http://localhost:9000/api/import/subscribers{ data: { name: , total: 0, imported: 0, status: none } }6.2 双重语义从StopImportSubscribers的注释与 Importer.Stop 的实现看该端点有两个行为分支有导入在运行importing向stop通道发送信号状态置为stopping。CSV 读取循环在每次迭代时通过select检查该信号importer.go收到后关闭队列并停止读取剩余未处理行不再导入没有导入在运行直接重置为none状态相当于“清除”上一次导入的残留状态使系统可以开启新导入。前端行为与之一致导入完成后按钮文案变为“Done”点击即调用该接口清除状态Import.vue 的stopImport方法。七、源码级原理导入会话如何把 CSV 写入数据库7.1 单例 Importer 与会话模型Importer被设计为有状态单例包注释明确说明每个 Importer 实例同时只允许一个导入见 importer.go。初始化发生在 cmd/init.go 的initImporter它将三张预编译 SQL 语句注入导入器UpsertStmt来自 queries/subscribers.sql 的upsert-subscriberBlocklistStmt来自 queries/subscribers.sql 的upsert-blocklist-subscriberUpdateListDateStmtupdate-lists-date导入完成后刷新相关列表的更新时间。同时注入privacy.domain_blocklist与privacy.domain_allowlist配置在config.toml.sample中可查看对应配置项用于导入时的邮箱域名过滤并通过PostCB回调在导入结束后刷新物化视图统计并向管理员发送通知。每次 POST 上传会创建一次Session核心结构是一个容量为commitBatchSize的 channel 队列subQueueLoadCSV协程负责解析、清洗、校验每一行将SubReq压入队列importer.goStart协程从队列取记录攒够commitBatchSize10000 条定义于 importer.go后在一个数据库事务中批量提交importer.go。7.2 两种模式的 SQL 差异subscribe 模式执行upsert-subscriber以email为冲突键做 UPSERT。$7overwrite_userinfo控制是否覆盖已存在记录的 name/attributes$8overwrite_subscription_status控制是否覆盖subscriber_lists中订阅状态插入记录统一为enabled状态订阅状态由列表关联表表达。ON CONFLICT (email)意味着同一文件内或与库中重复的邮箱只会产生一条记录blocklist 模式执行upsert-blocklist-subscriber将订阅者含已存在者状态置为blocklisted并把其所有现有订阅标记为unsubscribed——这正对应“从所有列表中移除并拉黑”的语义。7.3 行级校验与清洗每一行在入队前经过ValidateFields/SanitizeEmailimporter.go邮箱先经utils.SanitizeEmail校验并归一化长度超过 1000 视为非法校验通过后统一转为小写若命中配置的域名黑名单或不在白名单内该行被拒绝黑/白名单支持*.example.com形式的通配子域名makeDomainMapimporter.go姓名为空时从邮箱用户名部分派生按.与空格分词并做 Title Case如john.doeexample.com→John Doeattributes列内容按 JSON 解析解析失败仅记录日志并跳过该属性不中断导入importer.go。7.4 进度与完成回调每提交一个 10000 条批次imported计数增加一次队列关闭后若仍有残留记录则提交最后一个事务。随后状态置为finished或failed执行UpdateListDateStmt更新目标列表时间戳触发PostCBinit.go刷新物化视图订阅者计数、统计并发送管理员通知通知模板可在 static/email-templates/import-status.html 中查看。八、错误处理与失败行跳过策略导入器对“脏数据”采取跳过并继续的策略而非整体失败。常见场景及对应日志场景行为某行列数少于表头列数记录日志skipping line N. column count ... does not match跳过该行CSV 解析错误字段数不匹配的csv.ErrFieldCount跳过该行邮箱非法 / 命中域名黑名单跳过该行attributes不是合法 JSON跳过属性保留邮箱与姓名ZIP 中没有 CSV、文件为空、表头无email列导入置为failed因此调用方应以“日志接口中的跳过提示”作为数据质量排查依据而imported / total的差值即为被跳过的行数。九、与前端 / 官方客户端的联动参考管理后台的导入页面frontend/src/views/Import.vue是这套 API 最直接的参考实现展示了推荐的调用时序页面加载即调用 GET 状态接口none时展示上传表单否则展示进度条上传成功后以250ms 间隔轮询状态接口直到状态离开importing/stopping每次轮询同时拉取日志接口并滚动到底部进度条 Math.ceil((imported / total) * 100)完成或失败后点击按钮调用 DELETE 清除状态。前端提交的params与本文 3.1 节参数表一一对应见 Import.vue 的onSubmit方法其中lists传的是所选列表的 ID 数组。若需在自有脚本中复刻这一流程直接照此顺序调用四个端点即可。十、实战要点速查认证所有端点都需要subscribers:import权限使用 Basic Auth并发同一时刻仅允许一个导入会话重复 POST 会收到 400文件格式CSV 表头只需email必填、name、attributes列序任意支持单 CSV 或含单个 CSV 的 ZIPsubscribe模式必须指定lists否则 403blocklist模式无需列表覆盖语义默认不覆盖已存在订阅者的资料与状态overwrite: true等价于两个细粒度覆盖开关同时开启进度判断total是“总行数减表头”非法行会被跳过最终imported可能小于total请结合日志接口确认原因状态清理导入结束后调用 DELETE 将状态重置为none才能开启下一次导入。【免费下载链接】listmonkHigh performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.项目地址: https://gitcode.com/GitHub_Trending/li/listmonk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表