项目:X-Pan | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
《03-接口设计》讲契约总纲、《15-接口全量参考》是 89 个端点的速查表,本文补第三块拼图:10 个核心接口的完整请求/响应示例,JSON 全部按实体字段逐一构造(字段名与 framework/core、各模块 PO/VO 源码一一核对),并配可直接执行的 curl。三条约定先行:其一,示意值标注——示例中的加密 ID、token、时间戳均为编造的示意值(下文逐处标注),真实值由 XPAN_AES_KEY/XPAN_AES_IV 与签发时刻决定,不可复现;其二,统一响应体——所有业务接口返回 R{code,message,data}(framework/core/src/main/java/com/xiaoye/pan/core/response/R.java,@JsonInclude(NON_NULL) 使 null 字段整体消失),业务失败 HTTP 状态码仍是 200;其三,路径前缀——示例按 8081 直连形态书写,经 Nginx/Vite 时加 /api 前缀(见《03-接口设计》第 3 节)。加密 ID 的形态值得一提:AES-128 按 16 字节分组加密 8 字节的 Long(带填充恰成一个分组),密文再经无填充 URL-safe Base64 编码,呈现为 22 字符左右的 -_ 字符集字符串——下文如 q3Z1xP8vN2mK5jR7wY9cAa 均为示意值。
契约:免登录(@LoginIgnore),限流 5 次/分钟;入参 UserLoginPO(modules/user/po/UserLoginPO.java:username 正则 ^[0-9A-Za-z]{6,16}$、password 长度 8-16);clientId 不在 body 里,由请求头 X-Client-Id 携带(UserController.login 第 158 行 request.getHeader(...) 读入 Context)——这是"多端并存会话槽位" USER_LOGIN_{userId}_{clientId} 的另一半键值,Web 存 Cookie、移动端存 Keychain/Keystore 且终身稳定。响应 data 是 JWT 字符串本身(R.data(accessToken))。
curl -s -X POST http://localhost:8081/user/login \
-H "Content-Type: application/json" \
-H "X-Client-Id: web-8f3a2b1c" \
-d '{"username":"zhangsan","password":"P@ssw0rd123"}'成功响应(data 为 HS256 JWT 示意值,真实 token 含 userId/clientId/RENEWAL_TIME/exp 四组 claim):
{ "code": 0, "message": "SUCCESS", "data": "eyJhbGciOiJIUzI1NiJ9.eyJMT0dJTl9VU0VSX0lEIjoxOTI2NDk5MjMxNzQ4LCJDT...(示意值,已截断)" }两类失败(code 均为 1,文案区分,前端据此决定是否提示等待):密码错误 {"code":1,"message":"密码信息不正确"};连续失败 5 次后的锁定窗口内 {"code":1,"message":"登录失败次数过多,账号已被锁定15分钟"}。锁定按用户名维度的 Redis 计数实现,15 分钟自动解除。此后所有登录态请求携带 Authorization: <token>;响应头出现 New-Access-Token 时必须替换本地存储(续期协议见《08-安全与权限设计》第 1 节)。登录成功还有两个不易察觉的副作用值得调用方知晓:其一,UserController.login 在签发 token 后异步记录登录设备(RecordDeviceContext 携带 UA/IP,失败仅 warn 不影响登录主流程),设备管理页的数据即来源于此;其二,clientId 若首次出现会在 Redis 中登记进 USER_CLIENT_IDS_{userId} 活跃集合——改密时全端下线遍历的就是这个集合,因此"用新设备登录后改密,旧设备是否被踢"完全取决于该设备是否登录过。
契约:登录态。响应 UserInfoVO(modules/user/vo/UserInfoVO.java),rootFileId 标注 @JsonSerialize(using = IdEncryptSerializer.class) 自动加密——前端初始化文件列表时以它作为首个 parentId(《02-页面设计》1.1 守卫流程)。
curl -s http://localhost:8081/user/ -H "Authorization: eyJhbGciOiJIUzI1NiJ9..."{
"code": 0,
"message": "SUCCESS",
"data": {
"username": "zhangsan",
"rootFileId": "q3Z1xP8vN2mK5jR7wY9cAa",
"rootFilename": "全部文件",
"usedSize": 36892345118,
"totalSize": 107374182400,
"avatar": "avatar/2026/05/17/9f2c8e.png"
}
}字段说明:usedSize/totalSize 为字节(示意值:约 34.4 GB / 默认 100 GB,total_size 默认值 107374182400 见《04-数据模型》2.2);avatar 为存储引擎对象名可空。未登录(token 吊销/过期/缺失)时返回 {"code":10,"message":"NEED_LOGIN"}——10 是全端统一重登录信号,两端拦截器都只认它。
契约:登录态;入参 SecUploadFilePO(modules/file/po/SecUploadFilePO.java)实为三字段 parentId/filename/identifier(⚠️ 曾与《03-接口设计》2.2 表所写"含 totalSize"不一致,以源码为准;《03》2.2 与《15》§2 的同口径表述已在 2026-10-06 回刷中按本节修订)。响应语义已核实 FileController.secUpload(第 159-168 行):命中返回 R.success()——code=0 且无 message 无 data;未命中返回 R.fail("文件唯一标识不存在,请手动执行文件上传")(code=1)。即"未命中"不是异常而是协议内的正常分支信号,客户端按 code 分支:移动端 _trySecUpload(upload_service.dart)以 code=0 判命中、code=1 回退分片;Web 端拦截器把 code≠0 统一导入 reject 回调转入分片上传——注意 useUploader.ts 的 resolve 分支判定是 code === 0 && res.data,而命中响应无 data,Web 端命中后实际也会转入分片重传(客户端实现现状,联调时留意)。
curl -s -X POST http://localhost:8081/file/sec-upload \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." -H "Content-Type: application/json" \
-d '{"parentId":"k8Mn2PqR4sTuV6wX","filename":"架构设计说明书-v3.pdf","identifier":"3f5a8c2e91d04b67a8d1c0e9f2b3456a"}'{ "code": 0 }失败即转分片:{"code":1,"message":"文件唯一标识不存在,请手动执行文件上传"}。identifier 是客户端对全文件计算的 32 位 MD5 十六进制(示意值),它同时是分片路径与秒传键(《05-核心流程》流程一)。命中时服务端已在 SEC_UPLOAD_IDENTIFIER_LOCKS 锁内完成"查物理记录 + 挂逻辑引用",零字节传输完成上传且配额照常校验(ADR-007)。
契约:登录态,multipart/form-data;入参 FileChunkUploadPO(字段与校验注解原文见第 4 节代码块):identifier 强制 32 位 MD5 正则(防路径穿越)、totalChunks ≥1、totalSize 上限 3GB(@Max(3221225472L))、file 允许为 null(testChunks 探测请求)。响应 FileChunkUploadVO{mergeFlag}:0 无需合并、1 可合并(仅提示,真正合并仍由客户端显式调 /file/merge)。
curl -s -X POST http://localhost:8081/file/chunk-upload \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." \
-F "filename=架构设计说明书-v3.pdf" \
-F "identifier=3f5a8c2e91d04b67a8d1c0e9f2b3456a" \
-F "totalChunks=3" -F "chunkNumber=2" \
-F "currentChunkSize=5242880" -F "totalSize=12058624" \
-F "file=@chunk-2.bin"{ "code": 0, "message": "SUCCESS", "data": { "mergeFlag": 0 } }失败分支三个(文案均经 FileChunkServiceImpl 核实):{"code":1,"message":"分片下标越界"}(chunkNumber 不在 [1, totalChunks]);{"code":1,"message":"文件分片保存繁忙,请稍后重试"}(三重锁防御重试用尽,客户端安全重试);{"code":1,"message":"文件分片上传失败"}(对象写入或落库异常)。分片写入 MinIO 后路径为 x-pan-chunks/{identifier}/{chunkNumber},记录落 x_pan_file_chunk(联合唯一键保证重复请求幂等)。
契约:登录态;断点续传的"真相来源"——跨会话/跨设备恢复时先查它再决定补传哪些片。响应 UploadedChunksVO{uploadedChunks:List<Integer>},查询条件与过期判定一致(UserFileServiceImpl.getUploadedChunks 第 334-349 行:identifier + create_user + 未过期)。
curl -s "http://localhost:8081/file/chunk-upload?identifier=3f5a8c2e91d04b67a8d1c0e9f2b3456a&totalChunks=3" \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..."{ "code": 0, "message": "SUCCESS", "data": { "uploadedChunks": [1, 2] } }客户端语义:只补传缺失片(示意值表示 3 号片待传);已传分片过期的不会再出现在列表里(过期时间 = 上传时刻 + chunkFileExpirationDays)。前端 checkChunkUploadedByResponse 严格按此列表判定,接口异常时强制全量重传防合成坏文件(《12-FAQ与故障排查》第 4 节)。
契约:登录态;入参 FileChunkMergePO(filename/identifier/totalSize ≤3GB/parentId)。服务端先做连续性兜底校验(FileServiceImpl.doMergeFileChunk:"文件分片不完整,无法合并"),再引擎合并(composeObject 快路径 / 下载拼接慢路径,见走读篇)、落 x_pan_file + x_pan_user_file 两表。
curl -s -X POST http://localhost:8081/file/merge \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." -H "Content-Type: application/json" \
-d '{"filename":"架构设计说明书-v3.pdf","identifier":"3f5a8c2e91d04b67a8d1c0e9f2b3456a","totalSize":12058624,"parentId":"k8Mn2PqR4sTuV6wX"}'{ "code": 0, "message": "SUCCESS" }大文件合并耗时可达数十秒(任务面板 MERGE 态),完成推送走 WebSocket(第 11 节帧示例)。失败分支:{"code":1,"message":"文件分片不完整,无法合并"}、{"code":1,"message":"该文件未找到分片记录"}、配额类(秒传在合并层命中时同样走 saveUserFile 的配额校验)。
契约:登录态;响应 PageVO<XPanUserFileVO>(PageVO 字段:records/total/pageNum/pageSize/hasMore)。排序参数必须传 SafeOrderBy 白名单 snake_case 字段(filename/file_size/update_time/create_time),非法字段不报错但不生效。
curl -s "http://localhost:8081/files?parentId=q3Z1xP8vN2mK5jR7wY9cAa&fileType=5&orderBy=update_time&order=desc&pageNum=1&pageSize=50" \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..."{
"code": 0, "message": "SUCCESS",
"data": {
"records": [
{
"fileId": "m2Np4Qr6St8uV0wXyZ2b4d",
"parentId": "q3Z1xP8vN2mK5jR7wY9cAa",
"filename": "架构设计说明书-v3.pdf",
"fileSizeDesc": "11.5 MB",
"folderFlag": 0,
"fileType": 5,
"thumbnail": null,
"updateTime": "2026-10-05 21:47:12",
"deleteTime": null
},
{
"fileId": "c4Ee6Gg8Hi0jK2lMn4o6p",
"parentId": "q3Z1xP8vN2mK5jR7wY9cAa",
"filename": "项目资料",
"fileSizeDesc": "--",
"folderFlag": 1,
"fileType": 0,
"thumbnail": null,
"updateTime": "2026-09-30 09:12:03",
"deleteTime": null
}
],
"total": 27,
"pageNum": 1,
"pageSize": 50,
"hasMore": false
}
}字段说明(对照 XPanUserFileVO):fileId/parentId 出参均为加密 ID(示意值);fileSizeDesc 文件夹恒为 "--";fileType 5=pdf(全枚举见《04-数据模型》2.2);updateTime 经 Date2StringSerializer 格式化为 yyyy-MM-dd HH:mm:ss 示意值;thumbnail 未生成时为 null(NON_NULL 序列化下该键消失,示例保留 null 仅为阅读)。回收站页复用本 VO(GET /recycles),届时 deleteTime 非空并据此显示 30 天倒计时。分页语义上后端与前端有一处明确的职责分工:hasMore 由服务端按"当前页是否取满"计算,前端滚动加载以它为准而不是自己拿 total 除 pageSize 推算——两种算法在"浏览中途新增/删除文件"时会分叉,前端若自行推算会多加载或漏加载一页。排序参数若既不传白名单字段也不传方向,服务端按覆盖索引的默认序返回(约等于按 file_id 升序),这与界面默认的"按更新时间倒序"不同,联调时不要把"没传排序参数得到乱序"当成 bug。
契约:登录态 + 限流 30 次/分钟;入参 CreateShareUrlPO(shareName/shareType/shareDayType/shareFileIds 必填;shareCode 4-8 位可空 = 无提取码;downloadLimit 0 = 不限)。服务端 checkShareFileOwnership 逐文件校验归属并拒绝保险箱文件(parentId=-2)。响应 XPanShareUrlVO。
curl -s -X POST http://localhost:8081/share \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." -H "Content-Type: application/json" \
-d '{"shareName":"设计终稿交付","shareType":0,"shareDayType":1,"shareFileIds":["m2Np4Qr6St8uV0wXyZ2b4d"],"shareCode":"x7k2","downloadLimit":0}'{
"code": 0, "message": "SUCCESS",
"data": {
"shareId": "a9Bc1De3Fg5Hi7Jk9Lm1No",
"shareName": "设计终稿交付",
"shareUrl": "https://pan.example.com/share/a9Bc1De3Fg5Hi7Jk9Lm1No",
"shareCode": "x7k2",
"shareStatus": 0
}
}字段说明:shareDayType 0 永久/1 七天/2 三十天(永久时 share_end_time 落库为 2099-12-31,规避 NULL 判断);shareUrl 的 host 由 XPAN_SHARE_HOST 配置决定,域名示意;shareId 为加密 ID 示意值。错误分支:含保险箱文件或文件归属非本人返回 code=1(文案具体到原因);触发限流返回"操作过于频繁"。
契约:免登录(有防爆破计数,连错锁定 15 分钟);入参 CheckShareCodePO{shareId, shareCode}。响应 data 是独立于登录态的 Share-Token JWT(claim 携带 shareId,ShareServiceImpl.generateShareToken 第 723-725 行:有效期 1 小时)。此后分享域接口以 Share-Token 请求头携带。
curl -s -X POST http://localhost:8081/share/code/check \
-H "Content-Type: application/json" \
-d '{"shareId":"a9Bc1De3Fg5Hi7Jk9Lm1No","shareCode":"x7k2"}'{ "code": 0, "message": "SUCCESS", "data": "eyJhbGciOiJIUzI1NiJ9.eyJTSEFSRV9JRCI6IjE5MjY0OTkyMzE3NDkiLCJ...(示意值,已截断)" }失效三态按 code 区分(分享页各自错误态,客户端不应重试):文件丢失 code=5、已取消 code=6、已过期 code=7。三态示例(同一接口的三种失败响应,仅 message 随枚举变化,此处并列展示):
{ "code": 6, "message": "分享已被取消" }{ "code": 7, "message": "分享已过期" }{ "code": 5, "message": "分享的文件丢失" }提取码错误返回 code=1 + 防爆破文案;连错达阈值锁定 15 分钟(recordShareCodeFail,原生 INCR + 仅首次 EXPIRE)。凭 Share-Token 可再调 GET /share(详情)、GET /share/file/list(浏览)、GET /share/file/download(匿名下载);POST /share/save(转存)还需叠加登录态 Authorization——全系统唯一的双凭证接口(《05-核心流程》流程三)。
删除进回收站(软删除):DELETE /file,入参 DeleteFilePO{fileIds}(加密 ID 数组,必填非空),服务端递归收集子树置 del_flag=1 + delete_time,配额不减(回收站仍占额)。还原:PUT /recycle/restore,入参 RestorePO{fileIds},目标目录存在同名未删除文件时报业务 1(checkRestoreFilename 拦截)。
curl -s -X DELETE http://localhost:8081/file \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." -H "Content-Type: application/json" \
-d '{"fileIds":["m2Np4Qr6St8uV0wXyZ2b4d","c4Ee6Gg8Hi0jK2lMn4o6p"]}'{ "code": 0, "message": "SUCCESS" }curl -s -X PUT http://localhost:8081/recycle/restore \
-H "Authorization: eyJhbGciOiJIUzI1NiJ9..." -H "Content-Type: application/json" \
-d '{"fileIds":["m2Np4Qr6St8uV0wXyZ2b4d"]}'{ "code": 0, "message": "SUCCESS" }还原冲突示例:{"code":1,"message":"文件名重复"}(实际文案以 RecycleServiceImpl 为准,RecycleTest 的 testFileRestoreFailByWrongFilename1/2 固化了两个冲突分支)。彻底删除用 DELETE /recycle(入参 DeletePO,同样 fileIds 数组),触发级联清理与 FilePhysicalDeleteEvent——物理对象是否真删取决于引用计数(秒传复用方存在时仅删逻辑记录,见《05-核心流程》流程五)。两步之间的状态语义要分清:删除进回收站后配额 used_size 不减(文件仍占额,防止"删 100GB 腾空间"的错觉),彻底删除且引用归零后才扣减;删除与还原的响应体都不携带回执详情(成功时无 data),调用方应以删除/还原后的重查结果(列表、配额、统计接口)为准而不是假设服务端回传了新状态。文件夹的删除与还原是子树级联操作(递归收集、整树还原,审计 #7 修复过"只还原顶层导致空目录"的缺陷),对子树内任一成员单独还原的行为以服务端级联逻辑为准,客户端不应自行拆分子树逐个调用。
通道 ws(s)://host/ws/notification?token=<JWT>,帧格式以 common/websocket/NotificationWebSocketHandler.java 类注释与实现为准。连接成功即收到确认帧(第 65 行 sendJson 原文,userId 为明文数字示意值):
{"type":"CONNECTED","payload":{"userId":1926499231748}}心跳为服务端 30 秒 {"type":"PING","ts":1728228000000}(ts 示意值),客户端回 {"type":"PONG"};90 秒无任何客户端消息被主动关闭。业务帧的 payload 为对应 VO 的 JSON,与 x_pan_notification.type 枚举一致,四个典型帧如下(UPLOAD_FINISHED 的 payload 为文件 VO,字段同第 7 节,此处仅列关键示意字段):
{"type":"UPLOAD_FINISHED","payload":{"fileId":"m2Np4Qr6St8uV0wXyZ2b4d","filename":"架构设计说明书-v3.pdf","folderFlag":0}}{"type":"OFFLINE_TASK_UPDATE","payload":{"taskId":"t7Uu9Ww1Xy3zA5b7C9d1e","status":2,"downloadedSize":12058624}}{"type":"SHARE_STATS_UPDATE","payload":{"shareId":"a9Bc1De3Fg5Hi7Jk9Lm1No","visitCount":12,"saveCount":3,"downloadCount":5}}{"type":"SYSTEM_NOTICE","payload":{"level":"info","message":"系统将于周日凌晨 2 点升级维护"}}消费端约定:除 OFFLINE_TASK_REMOVED(仅推送不落库)外,推送与通知中心落库成对出现,断线期间的消息经 GET /notification/list 补齐;payload 内 ID 为加密 ID 示意值,type 与 payload 字段的精确结构以 Notifier 各 push 方法序列化的 VO 为准(⚠️ 待确认:部分 payload 字段名未逐字段核对,联调时以 WS 帧面板实测为准)。
第一,示例中的加密 ID、JWT、shareUrl、时间戳、数字全部是示意值,不能作为回放数据,真实值依赖密钥与运行时状态;唯一可复用的是字段名与类型。第二,本文的失败响应只列"源码可证的文案分支",线上还可能遇到统一异常兜底:RuntimeException 在生产环境固定返回 {"code":1,"message":"系统异常,请稍后重试"}(堆栈走 ErrorLogEvent 异步落库不外泄),JSR-303 校验失败返回 code=3 + 第一条校验消息(WebExceptionHandler 映射表见《03-接口设计》第 5 节)——遇到这两类响应说明请求没走到业务分支,先查参数与 traceId。第三,接口演进遵循"只加不改"(《06-部署运行》4.4),本文是 v1.2 时点快照,与实际不符时以 dev profile 的 /doc.html 与源码为准并回改本文;第 3 节已示范了一处对旧篇目的修正(sec-upload 入参与响应语义),后续发现漂移照此办理。