SView Help Center
English (US)

生成预览链接

Author: sviewngts Last Updated: 2026-09-02 18:58

 

前提 :该文件已经同步到企业看图中
单件预览 : 直接上传单件,按接口文档中的描述传参即可(无需传递 mainFileName )
装配结构预览 : 需提前将所有装配件及主装配以压缩包(支持*.zip/*.rar)的方式上传到系统中,生成预览链接时,需指定待预览的主装配文件名 mainFileName 。
接口名 POST: /login-register/open-api/documents/preview-url
入参:(body传值) application/json
是否必传 默认值 描述 备注
fileId 是 - 文件的唯一ID 该文件在企业看图平台内的唯一ID,可在同步文件元信息接口返回值中获取
fileScope 是 - 文件归属域 我的空间->personal 
项目空间(企业空间)->project
projectCode 否 - 该项目在外部平台的唯一编码,需提前创建项目 fileScope为project时需要传递

 

urlType

 

是
-
 
链接类型
目前仅提供preview类型的链接
preview->标准预览链接
用途:内部查看、无权限校验 
headless->无头内嵌预览,仅包含三维区域
用途:专门用于外部平台嵌入,简化头部、仅包含三维区域

 

mainFileName

否 -

 

主装配全量文件名(包含后缀)

若上传文件为压缩包格式,则需传入该参数,用于指定压缩包内待预览装配结构的主装配文件名(支持*.zip/*.rar)
入参:(Header传值)
Authorization 是 - 传递AccessToken 格式:Bearer {AccessToken}
入参示例:(application/json)

 

{
    "fileId": "ee1df7ccc30848589f03c2b563d721c3",
    "projectCode": "proj_openapi_001",
    "fileScope": "project",
    "urlType": "preview",
    "mainFileName": "总装.SLDASM"
}
回参示例:(application/json)

 

{
    "code": 0,
    "msg": null,
    "data": {
        "previewUrl": "https://xxxxxxxx.xxx/xxx/sviewView3D",
        "taskId":"aaaaaaaaaaaa" // 若此参数不为空,则说明文件需要处理,前端可将此ID作为socketId与服务端建立webSocket链接,监听处理进度,处理成功后,服务端会向前端发送成功的消息并携带 previewUrl 地址
    },
    "ok": true
}
package com.hoteam.sview.demo;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import cn.hutool.core.util.JsonUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

/**
 * 生成预览链接调用 Demo
 * 接口:POST /login-register/open-api/documents/preview-url
 * 业务成功判定:code=0 成功,code≠0 取 msg 提示
 * 更新点:新增可选参数 mainFileName,压缩包装配预览时使用
 */
@Slf4j
public class PreviewUrlDemo {

    // ===================== 入参实体 =====================
    @Data
    public static class PreviewUrlReq {
        private String fileId;
        private String fileScope;
        private String projectCode;
        private String urlType;
        /**
         * 【可选参数】主装配全名(带后缀)
         * 若上传文件为压缩包格式(.zip/.rar),则需传入该参数,用于指定压缩包内待预览装配结构的主装配文件名
         * 非压缩包单件文件预览无需传递此字段
         */
        private String mainFileName;
    }

    // ===================== 返回体实体 =====================
    @Data
    public static class PreviewUrlResp {
        private Integer code;
        private String msg;
        private Boolean ok;
        private PreviewUrlData data;
    }

    @Data
    public static class PreviewUrlData {
        private String previewUrl;
        private String taskId;
    }

    // ===================== 核心调用方法 =====================
    public static PreviewUrlResp getPreviewUrl(String apiBaseUrl, String accessToken, PreviewUrlReq req) {
        if (StrUtil.isBlank(apiBaseUrl)) {
            log.warn("接口基础地址不能为空");
            return null;
        }
        if (StrUtil.isBlank(accessToken)) {
            log.warn("AccessToken不能为空");
            return null;
        }
        if (req == null
                || StrUtil.isBlank(req.getFileId())
                || StrUtil.isBlank(req.getFileScope())
                || StrUtil.isBlank(req.getUrlType())) {
            log.warn("预览请求必填参数缺失,req:{}", req);
            return null;
        }
        // fileScope为project时,projectCode必填校验
        if ("project".equals(req.getFileScope()) && StrUtil.isBlank(req.getProjectCode())) {
            log.warn("fileScope为project时,projectCode不能为空");
            return null;
        }

        String fullUrl = apiBaseUrl + "/login-register/open-api/documents/preview-url";

        try (HttpResponse response = HttpRequest.post(fullUrl)
                .header(Header.AUTHORIZATION, "Bearer " + accessToken)
                .header(Header.CONTENT_TYPE, "application/json")
                .body(JsonUtil.toJsonStr(req))
                .timeout(60_000)
                .execute()) {

            int status = response.getStatus();
            String respBody = response.body();
            log.info("获取预览链接响应 status=[{}], body=[{}]", status, respBody);

            if (status != HttpStatus.HTTP_OK) {
                log.warn("获取预览链接HTTP请求失败,响应状态码=[{}]", status);
                return null;
            }

            PreviewUrlResp resp = JsonUtil.parse(respBody, PreviewUrlResp.class);
            if (resp == null) {
                log.warn("预览接口返回JSON解析失败");
                return null;
            }

            if (resp.getCode() != null && resp.getCode() != 0) {
                String errMsg = StrUtil.isNotBlank(resp.getMsg()) ? resp.getMsg() : "未知业务异常";
                log.warn("获取预览链接业务失败,code=[{}], msg=[{}]", resp.getCode(), errMsg);
                return resp;
            }

            log.info("获取预览链接业务执行成功");
            return resp;

        } catch (Exception e) {
            log.error("调用获取预览链接接口异常", e);
            return null;
        }
    }

    // ===================== Main 调用示例 =====================
    public static void main(String[] args) {
        String baseApi = "https://xxx.xxx.com";
        String token = "你的AccessToken";

        // ============示例1:单件文件预览(不需要mainFileName)============
        PreviewUrlReq reqSingle = new PreviewUrlReq();
        reqSingle.setFileId("平台返回的文件唯一fileId");
        reqSingle.setFileScope("personal");
        reqSingle.setUrlType("preview");
        PreviewUrlResp resultSingle = getPreviewUrl(baseApi, token, reqSingle);

        // ============示例2:压缩包装配预览(传递mainFileName)============
        PreviewUrlReq reqZip = new PreviewUrlReq();
        reqZip.setFileId("平台返回的压缩包文件fileId");
        reqZip.setFileScope("project");
        reqZip.setProjectCode("proj_openapi_001");
        reqZip.setUrlType("preview");
        // 压缩包内主装配文件名,必须带后缀,例如:总装.SLDASM
        reqZip.setMainFileName("总装.SLDASM");

        PreviewUrlResp result = getPreviewUrl(baseApi, token, reqZip);
        if (result == null) {
            log.error("获取预览链接请求整体失败");
            return;
        }
        if (result.getCode() != 0) {
            log.error("预览链接生成失败,错误码:{}, 错误信息:{}", result.getCode(), result.getMsg());
            return;
        }

        PreviewUrlData data = result.getData();
        if (data == null) {
            log.warn("预览接口返回data节点为空");
            return;
        }

        // 场景1:预览链接不为空,直接使用
        if (StrUtil.isNotBlank(data.getPreviewUrl())) {
            log.info("预览链接获取成功,预览地址:{}", data.getPreviewUrl());
            return;
        }

        // 场景2:预览链接为空,存在taskId,需要转码监听
        if (StrUtil.isNotBlank(data.getTaskId())) {
            log.info("文件需要后台转码,taskId={},需建立Socket监听转码状态", data.getTaskId());
            String socketUrl = baseApi + "/emh/ws/info?socketId=" + data.getTaskId();
            log.info("Socket连接地址:{}", socketUrl);
            return;
        }

        log.warn("previewUrl 和 taskId 均为空,接口返回异常");
    }
}
建立socket连接
// socket地址
ws(s)://{baseURL}/emh/ws/info?socketId={taksId}
// 举例,若域名配置了oss安全证书,则需要以 wss:// 开头
// ws://10.1.201.7:9999/emh/ws/info?socketId=aaaaaaaa
// wss://www.baidu.com/emh/ws/info?socketId=aaaaaaaa
建立连接后,需要实时监听socket返回的消息,根据消息体中的status状态判断是否处理成功,根据 percent 来获取进度
消息体示例
 
消息体中的data参数值为String类型的字符串
{"code":0,"command":"UNCOMPRESS_FILE","data":"{\"status\":\"success\",\"percent\":\"100\",\"waitingLength\":\"0\",\"waitingTime\":\"0\"}"}
{"code":0,"command":"CONVERT_TASK","data":"{\"status\":\"processing\",\"waitingLength\":\"0\",\"waitingTime\":\"0\"}"}
{"code":0,"command":"CONVERT_TASK","data":"{\"status\":\"processing\",\"percent\":\"0\",\"waitingLength\":\"0\",\"waitingTime\":\"0\"}"}
{"code":0,"command":"CONVERT_TASK","data":"{\"status\":\"processing\",\"percent\":\"3\",\"waitingLength\":\"0\",\"waitingTime\":\"0\"}"}
{"code": 0,"command": "CONVERT_TASK","data": "{\"status\":\"success\",\"percent\":\"100\",\"waitingLength\":\"0\",\"waitingTime\":\"0\",\"previewUrl\":\"http://xxx.com/SView/sview3d.html?absoluteFileURL=t5TTERBU00uc3ZseA&fileType=svlx\"}"}
 
command
UNCOMPRESS_FILE : 说明当前处于文件解压阶段,后续状态为解压过程中的状态
CONVERT_TASK : 说明当前处于文件转换阶段,后续状态为转换过程中的状态
status
processing :处理中
success:成功
failed :失败
 
只有监听到 command 为 CONVERT_TASK ,status 为 success 时 ,才能说明文件处理成功,此时socket消息中会携带previewUrl,前端直接截取对应参数值即可完成预览连接的获取。
其他情况下,无论 command 是 UNCOMPRESS_FILE 还是 CONVERT_TASK,只要 status == failed,直接判定任务失败,结束监听
 
<!-- websocket客户端依赖 -->
<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.4</version>
</dependency>
package com.hoteam.sview.demo;

import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.JsonUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 文件转码 WebSocket 监听客户端
 * 配合 PreviewUrlDemo 使用,taskId 来自预览接口返回
 * 规则:
 *  1、任意command下 status=failed → 整个任务失败
 *  2、command=CONVERT_TASK && status=success →转码成功,取出previewUrl
 */
@Slf4j
public class PreviewWebSocketClient extends WebSocketClient {

    // 用于阻塞主线程,等待转码结果返回
    private final CountDownLatch latch = new CountDownLatch(1);
    // 最终结果
    private TranscodeResult transcodeResult;

    public PreviewWebSocketClient(URI serverUri) {
        super(serverUri);
    }

    /**
     * 阻塞等待转码结果,设置最长等待超时时间
     * @param timeout 超时数值
     * @param unit 时间单位
     * @return 转码结果
     * @throws InterruptedException 中断异常
     */
    public TranscodeResult waitForResult(long timeout, TimeUnit unit) throws InterruptedException {
        latch.await(timeout, unit);
        return transcodeResult;
    }

    @Override
    public void onOpen(ServerHandshake handshakedata) {
        log.info("WebSocket连接成功,地址:{}", getURI());
    }

    @Override
    public void onMessage(String message) {
        log.info("WebSocket收到原始消息:{}", message);
        try {
            // 1、解析外层消息
            WsOuterMsg outerMsg = JsonUtil.parse(message, WsOuterMsg.class);
            if (outerMsg == null || outerMsg.getCode() != 0) {
                log.warn("websocket返回业务异常,message:{}",message);
                return;
            }
            String command = outerMsg.getCommand();
            String dataStr = outerMsg.getData();
            if(StrUtil.isBlank(dataStr)){
                return;
            }
            // 2、解析data内层JSON字符串
            WsInnerMsg innerMsg = JsonUtil.parse(dataStr, WsInnerMsg.class);
            if(innerMsg == null){
                return;
            }
            String status = innerMsg.getStatus();
            Integer percent = innerMsg.getPercent();
            log.info("阶段:{}, 当前状态:{}, 进度:{}%",command,status,percent);

            // ========== 改动:任意阶段只要status=failed,直接判定整体失败 ==========
            if("failed".equals(status)){
                transcodeResult = new TranscodeResult(false,null,"任务执行失败,阶段:"+ command);
                log.error("任务失败,command:{}",command);
                this.close();
                latch.countDown();
                return;
            }

            // ========== 成功判定:仍然必须是 CONVERT_TASK + success ==========
            if("CONVERT_TASK".equals(command) && "success".equals(status)){
                // 转码成功,取出预览地址
                transcodeResult = new TranscodeResult(true,innerMsg.getPreviewUrl(),null);
                log.info("文件转码成功!预览链接 = {}",innerMsg.getPreviewUrl());
                // 关闭连接,唤醒等待线程
                this.close();
                latch.countDown();
            }

            // UNCOMPRESS_FILE 阶段的success只代表解压完成,不结束监听,继续等待后续CONVERT_TASK消息

        } catch (Exception e) {
            log.error("WebSocket消息解析异常,msg={}",message,e);
        }
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        log.info("WebSocket连接关闭,code={},reason={}",code,reason);
        // 如果断开但是还没拿到结果,判定异常
        if(transcodeResult == null){
            transcodeResult = new TranscodeResult(false,null,"WebSocket提前断开连接");
            latch.countDown();
        }
    }

    @Override
    public void onError(Exception ex) {
        log.error("WebSocket通信异常",ex);
        transcodeResult = new TranscodeResult(false,null,"WebSocket发生异常:"+ex.getMessage());
        latch.countDown();
    }


    // ===================== 实体类 =====================
    /**
     * WebSocket外层消息体
     */
    @Data
    public static class WsOuterMsg{
        private Integer code;
        private String command;
        /** data为JSON字符串,需要二次解析 */
        private String data;
    }

    /**
     * data内层消息体
     */
    @Data
    public static class WsInnerMsg{
        /** processing / success / failed */
        private String status;
        private Integer percent;
        private String waitingLength;
        private String waitingTime;
        /** 转码成功后返回预览url */
        private String previewUrl;
    }

    /**
     * 最终转码返回结果对象
     */
    @Data
    public static class TranscodeResult{
        private Boolean success;
        private String previewUrl;
        private String errorMsg;
        public TranscodeResult(Boolean success,String previewUrl,String errorMsg){
            this.success = success;
            this.previewUrl = previewUrl;
            this.errorMsg = errorMsg;
        }
    }

    // ===================== 调用示例 =====================
    public static void main(String[] args) throws Exception {
        String baseApi = "https://xxx.xxx.com";
        // 从上一步预览接口拿到的taskId
        String taskId = "aaaaaaaaaaaaaa";
        String socketUrl = baseApi + "/emh/ws/info?socketId=" + taskId;

        PreviewWebSocketClient wsClient = new PreviewWebSocketClient(new URI(socketUrl));
        wsClient.connect();

        // 阻塞等待转码结果,最长等待10分钟
        TranscodeResult result = wsClient.waitForResult(10, TimeUnit.MINUTES);

        if(result.getSuccess()){
            log.info("最终预览地址:{}",result.getPreviewUrl());
        }else{
            log.error("转码失败:{}",result.getErrorMsg());
        }
    }
}