消费记录查询与异步下载

管理费用列表限额、历史消费异步查询、任务状态与安全下载接口

历史消费记录:异步查询与下载

使用当前任务明确授权的账号和筛选范围。以下路径相对 API Base URL;域名部署通常为 https://<站点>/api,后端直连通常不带 /api。这组接口不在 /v1 下,也不使用 x-async。不要借用普通用户会话、打印令牌或下载票据,不自动扩大时间、用户或历史任务范围。

交互列表的边界

POST /expense/admin/getMany 保留后台身份、admin.operations.expenses 菜单权限、分页与筛选契约,适用于管理员查看少量当前列表。请求示例:

{"view":"summary","pageNumber":1,"pageSize":20,"history_id":"example-history-id"}

共享 Redis 使用滚动 60 秒窗口:每个已认证管理员最多准入 12 次列表请求,同一资源总计 30 次,同时只允许一组列表读取(最多两个并行费用 find/count 命令)。同账号的浏览器、Python、Node 等客户端共用限额,不按 IP 或单个 PM2 进程计算。被拒绝的请求不发起费用 find/count,不排队;超额/并发忙返回 429、code=EXPENSE_QUERY_RATE_LIMITED 和 Retry-After 秒数。配额按已经准入的列表请求计数,包括随后失败或因连接断开而取消发起的读取;拒绝请求不额外扣配额。

每个交互费用 find、count 和明细 aggregate 设置 maxTimeMS=8000。这是数据库命令的执行上限,不是整个 HTTP 请求八秒内必定完成的承诺。数据库超时返回 504/EXPENSE_QUERY_TIME_LIMIT,应缩小条件或改用异步任务,禁止原条件快速循环重试。现有普通用户明细 aggregate 的十五秒上限保持不变。

429 时至少等待 Retry-After,最多进行一次后续明确重试;再次失败则停止交互查询。禁止并行请求、循环翻页抓取全量历史、客户端超时后连续重发。历史、批量、长时间范围或反复历史 ID 排查均走异步导出。不要调用旧的全量 /expense 或同步 CSV 接口来绕过这一边界。

保护服务异常(Redis 不可用、命令超时、权限不足或配置允许淘汰保护键)返回 503/EXPENSE_QUERY_PROTECTION_UNAVAILABLE、Retry-After: 5,不放开扫描。数据库连接错误无法确认扫描已结束时,返回 503/EXPENSE_QUERY_RESULT_UNCERTAIN,停止自动重试并通知管理员核对状态。Redis 准入等待期间 HTTP 已断开时,不再发起费用数据库查询,迟到的本请求令牌安全清理;HTTP 断开不释放仍在运行的查询,find/count 必须全部结束后才释放共享令牌。两条读取同时失败时,任一读取的网络/客户端错误都优先按结束状态未知处理,不能被另一条已确认的数据库超限错误覆盖;只有全部读取已确认结束时才允许释放。

费用异步查询的执行预算

管理员与个人的 expense 导出复用同一 worker:预估计数 aggregate 的数据库预算为 30 秒,主 aggregate 游标的累计数据库执行预算也为 30 秒,覆盖取得第一行及后续 getMore。计数已超限就停止该任务,不接着启动主扫描;主游标超限则关闭游标、删除未完成文件,任务进入 failed,error_code=EXPORT_EXPENSE_QUERY_TIME_LIMIT,不给下载票据,也不触发队列对同一条件的自动重试。该错误应缩小时间或筛选范围,经用户明确授权后用新筛选意图提交任务,不要反复重试原条件。

30 秒留给历史查询比交互列表更多的数据库执行时间,同时阻止一次缺少索引的扫描持续数分钟。计数与主游标各自计时;这是 MongoDB 在中断检查点执行的数据库预算,不是整个导出任务、文件写入或 HTTP 请求必定 30 秒内完成。关联信息读取保留现有实现,不能据此声称全部关联查询也已限时。取消与资源检查仍在批次边界发生;等待第一行期间由数据库预算约束,取消请求不表示数据库已经停止。

队列设置的 concurrency=1 是每个 worker 实例的并发限制,不是跨 PM2/多实例全局只有一个导出。列表的共享保护只覆盖 /expense/admin/getMany,不能将它当作导出链路的全局锁。部署时应结合导出 worker 实际实例数评估容量。以上时限及超限终止仅针对费用导出,不改变其他导出类型。

权限与筛选范围

范围创建入口创建 JSON权限与数据范围
管理员POST /admin/exports{"type":"expense","filters":{...}}后台身份及费用菜单权限;可查询经授权的站点范围。任务详情、票据仅创建者可见,角色权限仍会再次检查。
普通用户POST /user/exports{"filters":{...},"columns":["power","history_ids"]}BASIC 登录权限;服务器强制只查询当前账号自己的记录。不要传 type、user_id 或管理员/组织字段。columns 可省略。

管理员支持:searchKeyword、expenseType、created_from、created_to、expense_origin、source、organization_id、package_id、package_query、open_api_key_id、open_api_key_query、history_id、invoice_status,以及按实际执行主平台筛选的 platform_id。平台条件与交互明细、汇总保持一致,不以报价平台代替实际执行平台。普通用户只接受其中的关键词、消费类型、起止时间、请求来源、API Key ID/名称查询和历史 ID 八种条件。created_from/to 使用毫秒时间戳,不要用秒。searchKeyword 是模糊匹配,不能当作精确用户隔离;管理员导出不支持 user_id 筛选。

历史 ID 同时匹配普通费用的 history_ids 和合并文本费用的 entries.history_ids;导出会展开匹配的实际消费明细,数量可与汇总列表不同。不要传分页参数期待导出单页;导出范围由 filters 决定。管理员使用固定 CSV 列,含用户信息、历史 ID、类型、金额/算力、Token、来源、模型、时间等。个人导出只含允许的个人列,不包含管理员用户联系信息。

创建 → 状态 → 票据 → 下载

管理员示例(个人场景将前缀改成 /user/exports,创建体去掉 type):

  1. POST /admin/exports,Bearer 为当前授权账号,设置稳定的 Idempotency-Key: expense-history:admin:<逻辑任务ID>:
    {"type":"expense","filters":{"history_id":"example-history-id","created_from":1791734400000,"created_to":1791820799999}}
    

    成功 HTTP 201 返回含 _id、status:"queued" 的任务。相同创建者、范围、类型及条件的活动/可下载任务会复用。同一幂等键固定绑定同一筛选意图,条件变化须明确使用新任务 ID。创建超时后先 GET /admin/exports?type=expense&page=1&pageSize=50 核对自己已有任务,再决定是否重发;不要并行重复创建。
  2. 每五秒串行 GET /admin/exports/<任务ID>,HTTP 200。queued/running 继续等待,ready 才能下载。最多轮询十分钟;到期保留任务 ID,稍后恢复查询,不创建替代任务。failed/cancelled/expired/consumed 均停止轮询。失败先查看脱敏 error_code/error_message,修正原因并经用户允许后才 POST /<任务ID>/retry。没有自动无限重试。
  3. POST /admin/exports/<任务ID>/download-ticket,Bearer + 空 JSON {}。HTTP 201 返回 {"ticket":"<一次性票据>","expiresIn":300}。票据五分钟有效,不写日志、聊天或 URL。
  4. POST /admin/exports/<任务ID>/download,JSON {"ticket":"<一次性票据>"}。该步骤以票据鉴权,无需 Bearer;票据必须匹配任务和 admin/user 范围。现有成功状态为 HTTP 201,响应是 CSV/ZIP 二进制,按 2xx 和文件类型处理,不当作 JSON。大文件按分片 CSV 打包 ZIP。成功下载会消费任务并使票据失效,禁止复用。
  5. 下载中断时先查询同一个任务。只有仍为 ready 才申请新票据;不要假定连接失败代表服务器已停止,或自动创建另一个导出。consumed 表示服务器已完成下载消费,核对本地文件;expired 表示文件过期。文件默认保留约 24 小时,具体以 expires_at 为准。若用户希望保留历史文件,由用户自行在授权私有目录保存。

个人任务另有每用户一个活动任务限制,不同筛选的新提交需间隔十秒;429 后等待服务提示并核对已有任务。401/403 停止并检查授权,404 检查任务归属/范围、过期或已消费票据,503 停止自动重试并检查队列或保护服务。不要更换身份、扩大权限或回退为列表循环抓取。

使用正式内容包的脚本

API 地址必须使用 HTTPS;只有明确的 localhost、127.0.0.1 或 ::1 本机集成允许 HTTP,内网 IP 或其他域名也不能使用明文 HTTP。脚本拒绝重定向、带账号密码的 URL、查询参数和片段,不提供跳过 TLS 校验的选项。

由用户在隐藏输入/私有进程环境中提供 EASYAI_API_BASE_URL 和 EASYAI_SESSION_TOKEN;不要把真实值写在命令、文档、日志或 Git 中。将所需 filters 写入私有 JSON 文件(0600),再运行:

node scripts/expense_history_export.mjs --scope admin \
  --task-id consumption-audit-example \
  --filters-file /private/authorized/filters.json \
  --output-dir /private/authorized/downloads

个人示例:

node scripts/expense_history_export.mjs --scope user \
  --task-id personal-consumption-example \
  --filters-file /private/authorized/filters.json \
  --columns power,history_ids \
  --output-dir /private/authorized/downloads

恢复已有任务(不创建新任务):

node scripts/expense_history_export.mjs --scope admin \
  --job-id 000000000000000000000001 \
  --output-dir /private/authorized/downloads

脚本创建/状态请求单次三十秒超时、每五秒串行轮询、每轮最多十分钟;下载最多三十分钟。它不自动重试错误、不循环 getMany,不覆盖已存在文件,下载目录 0700、文件 0600;只输出任务与文件元数据,不打印凭据、票据、筛选内容或消费行。筛选文件及接口响应的 JSON 解析失败只返回固定的脱敏操作提示,不打印原输入、响应片段或底层异常;网络及文件错误也使用脱敏提示。出现错误立即停止,保留已取得的任务 ID,检查文件格式/读取权限或服务响应,不自动重复创建。下载数据可能含个人和财务信息,不上传公开服务,不放入 Git,不在普通聊天中转发原始记录。

部署与恢复边界

共享保护使用现有缓存 Redis 的 host/port/DB,所有 PM2 进程和实例必须一致,并允许 Lua/INFO。保护状态绑定 Redis run_id:标识缺失或 Redis 重启后,仅在 Redis 中没有活动令牌时自动绑定当前标识;仍有活动令牌则返回 503。每次费用读取还必须取得 MongoDB expense_query_guards 中持久且不自动过期的共享许可,防止 Redis 数据丢失后与尚未结束的旧读取并发;许可占用时返回 429,不启动费用扫描。仓库 Redis 配置为 noeviction,接口逐次核验该策略,避免保护键被静默淘汰。频率键在窗口结束后过期;并发令牌不设置过期时间,只有读取实际结束才删除。释放失败会重试,新的查询受阻;正常停机等待已开始读取完成。

进程被强杀、Redis 保护数据丢失或数据库网络错误导致结束状态未知时,不能凭超时或 Redis 重启认定查询已停止,也不能自动删除持久许可或其他请求的保护令牌。必须由运维先确认相关只读数据库操作均已结束,再按受控恢复流程处理残留状态。不要通过轮询或手动清锁绕过保护。

代码合入与脚本验收不等于生产部署;线上功能以对应版本的发布记录和实际验收为准。部署与恢复遵循仓库生产发布流程,不通过接口调用绕过保护,也不在查询脚本中执行生产数据回填、删除或账务修改。