backend-api-requirements.md 9.3 KB

后端 WPS 回调接口需求文档

问题分析

当前错误

init error: AppInfoNotExists won't start session
GET https://o.wpsgo.com/api/v3/office/file/28/multiwatermark 403 (Forbidden)

根本原因

WPS SDK 在初始化时会:

  1. 向 WPS 服务器验证 appId
  2. WPS 服务器会回调我们的后端接口获取文件信息
  3. 如果回调失败,返回 AppInfoNotExists 错误

结论:必须实现后端回调接口,WPS SDK 才能正常工作。

必需的后端接口

1. 文件信息接口(最重要)

接口地址

GET /v1/3rd/file/info

请求参数(Query):

  • _w_appid: WPS 应用ID(SX20251229FLIAPDAPP)
  • _w_fileid: 文件ID(前端传递的 fileId)

请求头

X-WebOffice-Token: {前端传递的 token}
X-User-Query: {前端传递的 customArgs,JSON 格式}

响应格式

{
  "code": 0,
  "msg": "success",
  "data": {
    "file": {
      "id": "28",
      "name": "万颗星临床营养系统接口文档.pdf",
      "version": 1,
      "size": 1234567,
      "download_url": "http://img.tpidea.cn/2025/12/29/d94280895eac4a499da425a3564c69b7.pdf",
      "creator": {
        "id": "user_1",
        "name": "张三"
      },
      "create_time": 1735459200,
      "modify_time": 1735459200,
      "user_acl": {
        "rename": 1,
        "history": 1,
        "copy": 1,
        "export": 1,
        "print": 1,
        "read": 1,
        "update": 1,
        "comment": 1
      }
    }
  }
}

字段说明

  • id: 文件ID,必须与请求的 _w_fileid 一致
  • name: 文件名
  • version: 文件版本号,从 1 开始
  • size: 文件大小(字节)
  • download_url: 文件下载地址,必须是可访问的 HTTP/HTTPS URL
  • creator: 创建者信息
  • create_time: 创建时间(Unix 时间戳,秒)
  • modify_time: 修改时间(Unix 时间戳,秒)
  • user_acl: 用户权限配置
    • rename: 是否允许重命名(1=允许,0=不允许)
    • history: 是否允许查看历史版本
    • copy: 是否允许复制
    • export: 是否允许导出
    • print: 是否允许打印
    • read: 是否允许查看(必须为 1)
    • update: 是否允许编辑(1=允许,0=只读)
    • comment: 是否允许批注

2. WPS 应用配置

在 WPS 开放平台控制台配置

  1. 登录:https://open.wps.cn/
  2. 进入应用管理
  3. 找到应用:SX20251229FLIAPDAPP
  4. 配置回调地址:
    • 文件信息接口:https://你的域名/v1/3rd/file/info
    • 保存接口:https://你的域名/v3/3rd/files

3. 三阶段保存接口(可选,用于编辑保存)

阶段 1:准备上传

GET /v3/3rd/files/:file_id/upload/prepare

响应

{
  "code": 0,
  "data": {
    "digest_types": ["sha1", "md5"]
  }
}

阶段 2:获取上传地址

POST /v3/3rd/files/:file_id/upload/address

请求体

{
  "name": "文档.pdf",
  "size": 1234567,
  "digest": {
    "sha1": "abc123..."
  },
  "is_manual": true
}

响应

{
  "code": 0,
  "data": {
    "method": "PUT",
    "url": "https://your-oss.com/upload/file123",
    "headers": {},
    "send_back_params": {}
  }
}

阶段 3:上传完成通知

POST /v3/3rd/files/:file_id/upload/complete

请求体

{
  "request": {
    "name": "文档.pdf",
    "size": 1234567,
    "digest": { "sha1": "abc123..." },
    "is_manual": true
  },
  "response": {
    "status_code": 200,
    "headers": {}
  }
}

响应

{
  "code": 0,
  "data": {
    "id": "28",
    "name": "文档.pdf",
    "version": 2,
    "size": 1234567,
    "create_time": 1735459200,
    "modify_time": 1735459300,
    "creator_id": "user_1",
    "modifier_id": "user_1"
  }
}

实现优先级

P0(必须实现,否则无法使用)

  • ✅ 文件信息接口:GET /v1/3rd/file/info
  • ✅ WPS 控制台配置回调地址

P1(编辑功能需要)

  • 三阶段保存接口(如果只需要查看,可以暂不实现)

快速实现示例(Node.js/Express)

// 文件信息接口
app.get('/v1/3rd/file/info', async (req, res) => {
  const { _w_appid, _w_fileid } = req.query;
  
  // 验证 appId
  if (_w_appid !== 'SX20251229FLIAPDAPP') {
    return res.status(403).json({ code: 403, msg: 'Invalid appId' });
  }
  
  // 从数据库获取文件信息
  const document = await getDocumentById(_w_fileid);
  
  if (!document) {
    return res.status(404).json({ code: 404, msg: 'File not found' });
  }
  
  // 返回文件信息
  res.json({
    code: 0,
    msg: 'success',
    data: {
      file: {
        id: document.id.toString(),
        name: document.fileName,
        version: document.version || 1,
        size: document.fileSize || 0,
        download_url: document.url,
        creator: {
          id: document.creatorId?.toString() || 'user_1',
          name: document.creatorName || '未知用户'
        },
        create_time: Math.floor(new Date(document.createTime).getTime() / 1000),
        modify_time: Math.floor(new Date(document.updateTime).getTime() / 1000),
        user_acl: {
          rename: 1,
          history: 1,
          copy: 1,
          export: 1,
          print: 1,
          read: 1,
          update: 1,  // 1=可编辑,0=只读
          comment: 1
        }
      }
    }
  });
});

快速实现示例(Java/Spring Boot)

@RestController
@RequestMapping("/v1/3rd/file")
public class WpsFileController {
    
    @Autowired
    private DocumentService documentService;
    
    @GetMapping("/info")
    public ResponseEntity<?> getFileInfo(
        @RequestParam("_w_appid") String appId,
        @RequestParam("_w_fileid") String fileId
    ) {
        // 验证 appId
        if (!"SX20251229FLIAPDAPP".equals(appId)) {
            return ResponseEntity.status(403)
                .body(Map.of("code", 403, "msg", "Invalid appId"));
        }
        
        // 获取文件信息
        Document document = documentService.getById(Long.parseLong(fileId));
        
        if (document == null) {
            return ResponseEntity.status(404)
                .body(Map.of("code", 404, "msg", "File not found"));
        }
        
        // 构建响应
        Map<String, Object> response = new HashMap<>();
        response.put("code", 0);
        response.put("msg", "success");
        
        Map<String, Object> fileInfo = new HashMap<>();
        fileInfo.put("id", document.getId().toString());
        fileInfo.put("name", document.getFileName());
        fileInfo.put("version", document.getVersion() != null ? document.getVersion() : 1);
        fileInfo.put("size", document.getFileSize() != null ? document.getFileSize() : 0);
        fileInfo.put("download_url", document.getUrl());
        
        Map<String, Object> creator = new HashMap<>();
        creator.put("id", document.getCreatorId() != null ? document.getCreatorId().toString() : "user_1");
        creator.put("name", document.getCreatorName() != null ? document.getCreatorName() : "未知用户");
        fileInfo.put("creator", creator);
        
        fileInfo.put("create_time", document.getCreateTime().getTime() / 1000);
        fileInfo.put("modify_time", document.getUpdateTime().getTime() / 1000);
        
        Map<String, Integer> userAcl = new HashMap<>();
        userAcl.put("rename", 1);
        userAcl.put("history", 1);
        userAcl.put("copy", 1);
        userAcl.put("export", 1);
        userAcl.put("print", 1);
        userAcl.put("read", 1);
        userAcl.put("update", 1);  // 1=可编辑,0=只读
        userAcl.put("comment", 1);
        fileInfo.put("user_acl", userAcl);
        
        response.put("data", Map.of("file", fileInfo));
        
        return ResponseEntity.ok(response);
    }
}

测试方法

1. 使用 Postman 测试

GET http://localhost:8080/v1/3rd/file/info?_w_appid=SX20251229FLIAPDAPP&_w_fileid=28

2. 使用 curl 测试

curl "http://localhost:8080/v1/3rd/file/info?_w_appid=SX20251229FLIAPDAPP&_w_fileid=28"

3. 检查响应

确保响应格式完全符合上面的 JSON 格式。

常见问题

Q1: 仍然报 AppInfoNotExists

原因

  1. 后端接口未实现
  2. 接口地址配置错误
  3. 接口返回格式不正确
  4. WPS 控制台未配置回调地址

解决

  1. 确认接口已实现并可访问
  2. 检查 WPS 控制台的回调地址配置
  3. 使用 Postman 测试接口返回格式

Q2: 403 Forbidden

原因

  1. appId 验证失败
  2. 文件不存在
  3. 权限不足

解决

  1. 检查 appId 是否正确
  2. 检查 fileId 对应的文件是否存在
  3. 检查用户权限

Q3: 文件无法显示

原因

  1. download_url 无法访问
  2. 文件格式不支持
  3. 文件损坏

解决

  1. 确保 download_url 可以直接访问
  2. 检查文件格式是否正确
  3. 尝试直接下载文件验证

下一步行动

立即执行

  1. 实现 GET /v1/3rd/file/info 接口
  2. 使用 Postman 测试接口
  3. 在 WPS 控制台配置回调地址

验证步骤

  1. 重启后端服务
  2. 刷新前端页面
  3. 打开审核对话框
  4. 检查是否还有 AppInfoNotExists 错误
  5. 验证文档是否正常显示

参考资料