# 后端 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 格式}
```

**响应格式**：
```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
```

**响应**：
```json
{
  "code": 0,
  "data": {
    "digest_types": ["sha1", "md5"]
  }
}
```

#### 阶段 2：获取上传地址
```
POST /v3/3rd/files/:file_id/upload/address
```

**请求体**：
```json
{
  "name": "文档.pdf",
  "size": 1234567,
  "digest": {
    "sha1": "abc123..."
  },
  "is_manual": true
}
```

**响应**：
```json
{
  "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
```

**请求体**：
```json
{
  "request": {
    "name": "文档.pdf",
    "size": 1234567,
    "digest": { "sha1": "abc123..." },
    "is_manual": true
  },
  "response": {
    "status_code": 200,
    "headers": {}
  }
}
```

**响应**：
```json
{
  "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）

```javascript
// 文件信息接口
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）

```java
@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 测试
```bash
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. 验证文档是否正常显示

## 参考资料

- [WPS 开放平台](https://open.wps.cn/)
- [文件信息接口文档](https://solution.wps.cn/docs/callback/file.html)
- [三阶段保存文档](https://solution.wps.cn/docs/callback/save.html)
