diff --git a/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/H5OpenController.java b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/H5OpenController.java
index 44a77c9f..bc532661 100644
--- a/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/H5OpenController.java
+++ b/safety-eval-adapter/src/main/java/org/qinan/safetyeval/adapter/web/H5OpenController.java
@@ -1,122 +1,154 @@
package org.qinan.safetyeval.adapter.web;
-import cn.hutool.json.JSONUtil;
-import com.alibaba.fastjson2.JSON;
-import com.jjb.saas.framework.auth.model.SSOUser;
-import com.jjb.saas.framework.utils.JwtTokenUtils;
+import com.alibaba.cola.dto.SingleResponse;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
-import org.dromara.x.file.storage.core.FileInfo;
-import org.dromara.x.file.storage.core.FileStorageService;
-import org.qinan.safetyeval.infrastructure.mapper.AccountMapper;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.qinan.safetyeval.client.api.WpsApi;
+import org.qinan.safetyeval.client.co.WpsDocOpenCO;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
-import java.io.InputStream;
import java.util.List;
import java.util.Map;
/**
- * 对外开放接口(免鉴权)。
- *
- * 所有不需要登录验证的接口统一挂载在 /safetyEval/images 前缀下。
- *
+ * WPS WebOffice 服务器回调(免登录拦截,由 WPS 携带 token)。
+ *
+ * @author safety-eval
*/
-@Api(tags = "对外开放接口")
+@Api(tags = "WPS回调接口")
@RestController
@RequestMapping("/safetyEval-h5/wsp")
@Slf4j
public class H5OpenController {
- @Autowired
- private AccountMapper accountMapper;
- @Autowired
- private FileStorageService fileStorageService;
+ @Resource
+ private WpsApi wpsApi;
+ @Resource
+ private ObjectMapper objectMapper;
- @ApiOperation("wps回调预览")
+ @ApiOperation("wps回调-获取文件信息")
@GetMapping(value = "/v3/3rd/files/{file_id}", produces = MediaType.APPLICATION_JSON_VALUE)
- public String getWpsInfoByFileId(@PathVariable("file_id") String file_id, HttpServletRequest request) {
- log.info("token, {}", request.getHeader("token"));
- log.info("Token, {}", request.getHeader("Token"));
- String header = request.getHeader("X-Weboffice-Token");
- log.info("X-Weboffice-Token, {}", request.getHeader("X-Weboffice-Token"));
-
- String token = header;
- log.info("2.5token不为空:{}", token);
- token = token.replaceAll("jjb-saas-auth:oauth:", "").trim();
- String json = null;
- SSOUser ssoUser;
- try {
- json = JwtTokenUtils.parseToken(token);
- } catch (Exception var41) {
- }
- if (null != json) {
- ssoUser = (SSOUser) JSONUtil.toBean(json, SSOUser.class);
- } else {
- ssoUser = new SSOUser();
- }
- log.info("ssoUser:{}", ssoUser);
- log.info("wps回调预览:{}", file_id);
- return accountMapper.getWps(1);
+ public String getWpsInfoByFileId(@PathVariable("file_id") String fileId) {
+ log.info("wps回调预览:{}", fileId);
+ return toJson(wpsApi.getFileInfo(fileId));
}
- @ApiOperation("wps回调下载")
+ @ApiOperation("wps回调-获取下载地址")
@GetMapping(value = "/v3/3rd/files/{file_id}/download", produces = MediaType.APPLICATION_JSON_VALUE)
- public String getWpsDownloadByFileId(@PathVariable("file_id") String file_id) {
- log.info("wps回调下载:{}", file_id);
- return accountMapper.getWps(2);
+ public String getWpsDownloadByFileId(@PathVariable("file_id") String fileId) {
+ log.info("wps回调下载:{}", fileId);
+ return toJson(wpsApi.getDownload(fileId));
}
- @ApiOperation("wps回调权限信息")
+ @ApiOperation("wps回调-文档权限")
@GetMapping(value = "/v3/3rd/files/{file_id}/permission", produces = MediaType.APPLICATION_JSON_VALUE)
- public String getWpsPermissionByFileId(@PathVariable("file_id") String file_id) {
- log.info("wps回调权限信息:{}", file_id);
- return accountMapper.getWps(3);
+ public String getWpsPermissionByFileId(@PathVariable("file_id") String fileId) {
+ log.info("wps回调权限信息:{}", fileId);
+ return toJson(wpsApi.getPermission(fileId));
}
- @ApiOperation("wps回调用户信息")
+ @ApiOperation("wps回调-批量用户信息")
@GetMapping(value = "/v3/3rd/users", produces = MediaType.APPLICATION_JSON_VALUE)
- public String getWpsUsers(@RequestParam List user_ids) {
- log.info("wps回调用户信息:{}", user_ids);
- return accountMapper.getWps(4);
+ public String getWpsUsers(@RequestParam("user_ids") List userIds) {
+ log.info("wps回调用户信息:{}", userIds);
+ return toJson(wpsApi.getUsers(userIds));
}
- @ApiOperation("wps回调上传第一步")
+ @ApiOperation("wps回调-上传准备")
@GetMapping(value = "/v3/3rd/files/{file_id}/upload/prepare", produces = MediaType.APPLICATION_JSON_VALUE)
- public String getWpsUpLoadOne(@PathVariable("file_id") String file_id) {
- log.info("wps回调上传第一步:{}", file_id);
- return accountMapper.getWps(5);
+ public String getWpsUpLoadOne(@PathVariable("file_id") String fileId) {
+ log.info("wps回调上传第一步:{}", fileId);
+ return toJson(wpsApi.uploadPrepare(fileId));
}
- @ApiOperation("wps回调上传第二步")
- @PostMapping(value = "/v3/3rd/files/{file_id}/upload/address")
- public String getWpsUpLoadTwo(@PathVariable("file_id") String file_id, @RequestBody Map map) {
- log.info("wps回调上传第二步:{}, map: {}", file_id, JSON.toJSONString(map));
- return accountMapper.getWps(6);
+ @ApiOperation("wps回调-获取上传地址")
+ @PostMapping(value = "/v3/3rd/files/{file_id}/upload/address", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String getWpsUpLoadTwo(@PathVariable("file_id") String fileId,
+ @RequestBody Map body) {
+ log.info("wps回调上传第二步:{}, body={}", fileId, body);
+ return toJson(wpsApi.uploadAddress(fileId, body));
}
- @ApiOperation("wps回调上传第三步")
- @PostMapping(value = "/v3/3rd/files/{file_id}/upload/complete")
- public String getWpsUpLoadThree(@PathVariable("file_id") String file_id, @RequestBody Map map) {
- String msg = accountMapper.getWps(7);
- log.info("wps回调上传第三步:{}, map: {}", file_id, msg);
- return msg;
+ @ApiOperation("wps回调-上传完成")
+ @PostMapping(value = "/v3/3rd/files/{file_id}/upload/complete", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String getWpsUpLoadThree(@PathVariable("file_id") String fileId,
+ @RequestBody Map body) {
+ log.info("wps回调上传第三步:{}, body={}", fileId, body);
+ return toJson(wpsApi.uploadComplete(fileId, body));
}
- @PutMapping(value = "/upload/{file_id}", consumes = "application/octet-stream")
- public String uploadFile(@PathVariable("file_id") String file_id, HttpServletRequest request) throws IOException {
- // 获取文件的二进制流
- try (InputStream inputStream = request.getInputStream()) {
- FileInfo upload = fileStorageService.of(inputStream, file_id).upload();
- log.info("上传文件成功:{}", JSON.toJSONString(upload));
- return "ok";
- } catch (Exception e) {
- log.error("上传文件失败:{}", e.getMessage());
- return "上传文件失败";
+ @ApiOperation("wps回调-PUT上传文件实体")
+ @PutMapping(value = "/upload/{file_id}", consumes = "application/octet-stream", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String uploadFile(@PathVariable("file_id") String fileId, HttpServletRequest request) throws IOException {
+ log.info("wps PUT上传:{}", fileId);
+ return toJson(wpsApi.uploadBinary(fileId, request.getInputStream()));
+ }
+
+ @ApiOperation("wps回调-历史版本列表")
+ @GetMapping(value = "/v3/3rd/files/{file_id}/versions", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String getWpsVersions(@PathVariable("file_id") String fileId) {
+ log.info("wps回调历史版本列表:{}", fileId);
+ return toJson(wpsApi.listVersions(fileId));
+ }
+
+ @ApiOperation("wps回调-指定历史版本")
+ @GetMapping(value = "/v3/3rd/files/{file_id}/versions/{version}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String getWpsVersion(@PathVariable("file_id") String fileId,
+ @PathVariable("version") Integer version) {
+ log.info("wps回调历史版本详情:{}, version={}", fileId, version);
+ return toJson(wpsApi.getVersion(fileId, version));
+ }
+
+ @ApiOperation("wps回调-历史版本下载")
+ @GetMapping(value = "/v3/3rd/files/{file_id}/versions/{version}/download", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String getWpsVersionDownload(@PathVariable("file_id") String fileId,
+ @PathVariable("version") Integer version) {
+ log.info("wps回调历史版本下载:{}, version={}", fileId, version);
+ return toJson(wpsApi.getVersionDownload(fileId, version));
+ }
+
+ @ApiOperation("wps回调-文档重命名")
+ @PutMapping(value = "/v3/3rd/files/{file_id}/name", produces = MediaType.APPLICATION_JSON_VALUE)
+ public String renameWpsFile(@PathVariable("file_id") String fileId,
+ @RequestBody Map body) {
+ log.info("wps回调重命名:{}, body={}", fileId, body);
+ return toJson(wpsApi.rename(fileId, body));
+ }
+
+ @ApiOperation("打开页:当前用户有文档返回最近一份,没有则创建空白docx")
+ @GetMapping(value = "/doc/my-or-create", produces = MediaType.APPLICATION_JSON_VALUE)
+ public SingleResponse myOrCreate() {
+ log.info("wps my-or-create");
+ return SingleResponse.of(wpsApi.myOrCreate());
+ }
+
+ @ApiOperation("始终新建一份空白docx")
+ @PostMapping(value = "/doc/create", produces = MediaType.APPLICATION_JSON_VALUE)
+ public SingleResponse createBlank() {
+ log.info("wps create blank");
+ return SingleResponse.of(wpsApi.createBlank());
+ }
+
+ private String toJson(Map result) {
+ try {
+ return objectMapper.writeValueAsString(result);
+ } catch (JsonProcessingException e) {
+ log.error("wps response serialize failed", e);
+ return "{\"code\":40001,\"message\":\"serialize error\"}";
}
}
}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/convertor/WpsCmdConvertor.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/convertor/WpsCmdConvertor.java
new file mode 100644
index 00000000..20e59640
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/convertor/WpsCmdConvertor.java
@@ -0,0 +1,36 @@
+package org.qinan.safetyeval.app.convertor;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.qinan.safetyeval.client.co.WpsFileCO;
+import org.qinan.safetyeval.client.co.WpsUserCO;
+import org.qinan.safetyeval.domain.entity.AccountEntity;
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+
+/**
+ * WPS 回调对象转换(App层)
+ *
+ * @author safety-eval
+ */
+@Mapper(componentModel = "spring")
+public interface WpsCmdConvertor {
+
+ @Mapping(source = "fileId", target = "id")
+ @Mapping(source = "docVersion", target = "version")
+ @Mapping(source = "createTimeEpoch", target = "createTime")
+ @Mapping(source = "modifyTimeEpoch", target = "modifyTime")
+ WpsFileCO toFileCO(WpsDocEntity entity);
+
+ @Mapping(source = "fileId", target = "id")
+ @Mapping(source = "docVersion", target = "version")
+ @Mapping(source = "createTimeEpoch", target = "createTime")
+ @Mapping(source = "createTimeEpoch", target = "modifyTime")
+ @Mapping(target = "creatorId", ignore = true)
+ WpsFileCO toFileCO(WpsDocVersionEntity entity);
+
+ @Mapping(target = "id", expression = "java(entity.getId() == null ? null : String.valueOf(entity.getId()))")
+ @Mapping(source = "userName", target = "name")
+ @Mapping(source = "profileUrl", target = "avatarUrl")
+ WpsUserCO toUserCO(AccountEntity entity);
+}
diff --git a/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java
new file mode 100644
index 00000000..656c579c
--- /dev/null
+++ b/safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java
@@ -0,0 +1,480 @@
+package org.qinan.safetyeval.app.executor;
+
+import com.alibaba.fastjson2.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.x.file.storage.core.FileInfo;
+import org.dromara.x.file.storage.core.FileStorageService;
+import org.qinan.safetyeval.app.convertor.WpsCmdConvertor;
+import org.qinan.safetyeval.client.api.WpsApi;
+import org.qinan.safetyeval.client.co.WpsDocOpenCO;
+import org.qinan.safetyeval.client.co.WpsFileCO;
+import org.qinan.safetyeval.client.co.WpsPermissionCO;
+import org.qinan.safetyeval.client.co.WpsUserCO;
+import org.qinan.safetyeval.domain.entity.AccountEntity;
+import org.qinan.safetyeval.domain.entity.FileStorage;
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+import org.qinan.safetyeval.domain.exception.BizException;
+import org.qinan.safetyeval.domain.exception.ErrorCode;
+import org.qinan.safetyeval.domain.gateway.AccountGateway;
+import org.qinan.safetyeval.domain.service.FileStorageDomainService;
+import org.qinan.safetyeval.domain.service.WpsDocDomainService;
+import org.qinan.safetyeval.infrastructure.adapter.ThreadLocalUserInfoAdapter;
+import org.qinan.safetyeval.infrastructure.adapter.auth.AuthUserContextAdapter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.annotation.Resource;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * WPS WebOffice 回调执行器
+ *
+ * @author safety-eval
+ */
+@Slf4j
+@Service
+public class WpsExecutor implements WpsApi {
+
+ @Resource
+ private WpsDocDomainService wpsDocDomainService;
+ @Resource
+ private AccountGateway accountGateway;
+ @Resource
+ private WpsCmdConvertor wpsCmdConvertor;
+ @Resource
+ private FileStorageService fileStorageService;
+ @Resource
+ private FileStorageDomainService fileStorageDomainService;
+
+ @Value("${wps.upload-base-url:}")
+ private String uploadBaseUrl;
+
+ @Value("${wps.def-docx-url:}")
+ private String defDocxUrl;
+
+ @Override
+ public Map getFileInfo(String fileId) {
+ try {
+ WpsDocEntity doc = wpsDocDomainService.getByFileId(fileId);
+ return ok(wpsCmdConvertor.toFileCO(doc));
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map getDownload(String fileId) {
+ try {
+ WpsDocEntity doc = wpsDocDomainService.getByFileId(fileId);
+ Map data = new LinkedHashMap<>();
+ data.put("url", resolveDownloadUrl(doc.getDownloadUrl(), doc.getResourceId()));
+ return ok(data);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map getPermission(String fileId) {
+ try {
+ wpsDocDomainService.getByFileId(fileId);
+ Long userId = AuthUserContextAdapter.getCurrentUserId();
+ if (userId == null) {
+ throw new BizException(ErrorCode.WPS_USER_NOT_LOGIN);
+ }
+ WpsPermissionCO permission = new WpsPermissionCO();
+ permission.setUserId(String.valueOf(userId));
+ permission.setRead(1);
+ permission.setUpdate(1);
+ permission.setDownload(1);
+ permission.setPrint(1);
+ permission.setCopy(1);
+ permission.setComment(1);
+ // 未实现重命名回调,保持 0
+ permission.setRename(1);
+ permission.setHistory(1);
+ permission.setSaveas(1);
+ return ok(permission);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map getUsers(List userIds) {
+ try {
+ if (userIds == null || userIds.isEmpty()) {
+ return ok(Collections.emptyList());
+ }
+ List ids = userIds.stream()
+ .filter(StringUtils::hasText)
+ .map(Long::valueOf)
+ .distinct()
+ .collect(Collectors.toList());
+ List accounts = accountGateway.listByIds(ids);
+ List users = new ArrayList<>();
+ for (AccountEntity account : accounts) {
+ users.add(wpsCmdConvertor.toUserCO(account));
+ }
+ return ok(users);
+ } catch (BizException e) {
+ return fail(e);
+ } catch (Exception e) {
+ log.error("wps getUsers failed, userIds={}", userIds, e);
+ return fail(ErrorCode.UNKNOWN_ERROR);
+ }
+ }
+
+ @Override
+ public Map uploadPrepare(String fileId) {
+ try {
+ wpsDocDomainService.getByFileId(fileId);
+ Map data = new LinkedHashMap<>();
+ data.put("digest_types", Collections.singletonList("sha1"));
+ return ok(data);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map uploadAddress(String fileId, Map body) {
+ try {
+ wpsDocDomainService.getByFileId(fileId);
+ if (!StringUtils.hasText(uploadBaseUrl)) {
+ log.error("wps.upload-base-url is empty");
+ return fail(ErrorCode.UNKNOWN_ERROR);
+ }
+ String base = uploadBaseUrl.endsWith("/") ? uploadBaseUrl.substring(0, uploadBaseUrl.length() - 1) : uploadBaseUrl;
+ Map data = new LinkedHashMap<>();
+ data.put("url", base + "/upload/" + fileId);
+ data.put("method", "PUT");
+ return ok(data);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map uploadBinary(String fileId, InputStream inputStream) {
+ try {
+ wpsDocDomainService.getByFileId(fileId);
+ FileInfo ossFileInfo = fileStorageService.of(inputStream, fileId).upload();
+ FileStorage fileStorage = FileStorageHelper.buildResource(ossFileInfo, fileId, true);
+ FileStorage saved = fileStorageDomainService.saveResource(fileStorage);
+ wpsDocDomainService.savePendingUpload(fileId, saved.getId(), saved.getUrl(), saved.getSize());
+ log.info("wps upload binary success, fileId={}, resourceId={}, url={}", fileId, saved.getId(), saved.getUrl());
+ Map data = new LinkedHashMap<>();
+ data.put("status", "ok");
+ return ok(data);
+ } catch (BizException e) {
+ return fail(e);
+ } catch (Exception e) {
+ log.error("wps upload binary failed, fileId={}", fileId, e);
+ return fail(ErrorCode.FILE_UPLOAD_FAILED);
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Map uploadComplete(String fileId, Map body) {
+ try {
+ WpsDocEntity doc = wpsDocDomainService.getByFileId(fileId);
+ if (doc.getPendingResourceId() == null) {
+ throw new BizException(ErrorCode.WPS_UPLOAD_PENDING_NOT_FOUND);
+ }
+ Integer pendingResourceId = doc.getPendingResourceId();
+ String pendingUrl = doc.getPendingDownloadUrl();
+ Long pendingSize = doc.getPendingSize();
+
+ Long userId = AuthUserContextAdapter.getCurrentUserId();
+ String modifierId = userId == null ? doc.getModifierId() : String.valueOf(userId);
+ long nowEpoch = System.currentTimeMillis() / 1000;
+
+ int nextVersion = (doc.getDocVersion() == null ? 1 : doc.getDocVersion()) + 1;
+ String name = doc.getName();
+ Boolean isManual = null;
+ String digestSha1 = null;
+ if (body != null && body.get("request") instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map request = (Map) body.get("request");
+ if (request.get("name") != null) {
+ name = String.valueOf(request.get("name"));
+ }
+ if (request.get("is_manual") != null) {
+ isManual = Boolean.valueOf(String.valueOf(request.get("is_manual")));
+ }
+ Object digestObj = request.get("digest");
+ if (digestObj instanceof Map) {
+ Object sha1 = ((Map, ?>) digestObj).get("sha1");
+ if (sha1 != null) {
+ digestSha1 = String.valueOf(sha1);
+ }
+ }
+ }
+
+ doc.setName(name);
+ doc.setDocVersion(nextVersion);
+ doc.setSize(pendingSize == null ? doc.getSize() : pendingSize);
+ doc.setResourceId(pendingResourceId);
+ doc.setDownloadUrl(pendingUrl);
+ doc.setModifierId(modifierId);
+ doc.setModifyTimeEpoch(nowEpoch);
+
+ WpsDocVersionEntity versionEntity = new WpsDocVersionEntity();
+ versionEntity.setOrgId(doc.getOrgId());
+ versionEntity.setFileId(fileId);
+ versionEntity.setDocVersion(nextVersion);
+ versionEntity.setName(name);
+ versionEntity.setSize(doc.getSize());
+ versionEntity.setResourceId(pendingResourceId);
+ versionEntity.setDownloadUrl(pendingUrl);
+ versionEntity.setDigestSha1(digestSha1);
+ versionEntity.setModifierId(modifierId);
+ versionEntity.setManualFlag(Boolean.TRUE.equals(isManual) ? 1 : 2);
+ versionEntity.setCreateTimeEpoch(nowEpoch);
+ versionEntity.setTenantId(doc.getTenantId());
+
+ WpsDocEntity updated = wpsDocDomainService.completeSave(doc, versionEntity);
+ log.info("wps upload complete, body={}", JSON.toJSONString(body));
+ return ok(wpsCmdConvertor.toFileCO(updated));
+ } catch (BizException e) {
+ return fail(e);
+ } catch (Exception e) {
+ log.error("wps upload complete failed, fileId={}", fileId, e);
+ return fail(ErrorCode.FILE_UPLOAD_FAILED);
+ }
+ }
+
+ @Override
+ public Map listVersions(String fileId) {
+ try {
+ WpsDocEntity doc = wpsDocDomainService.getByFileId(fileId);
+ List versions = wpsDocDomainService.listVersions(fileId);
+ List list = new ArrayList<>();
+ for (WpsDocVersionEntity version : versions) {
+ WpsFileCO co = wpsCmdConvertor.toFileCO(version);
+ co.setCreatorId(doc.getCreatorId());
+ co.setModifierId(version.getModifierId());
+ list.add(co);
+ }
+ return ok(list);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map getVersion(String fileId, Integer version) {
+ try {
+ WpsDocEntity doc = wpsDocDomainService.getByFileId(fileId);
+ WpsDocVersionEntity versionEntity = wpsDocDomainService.getVersion(fileId, version);
+ WpsFileCO co = wpsCmdConvertor.toFileCO(versionEntity);
+ co.setCreatorId(doc.getCreatorId());
+ co.setModifierId(versionEntity.getModifierId());
+ return ok(co);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map getVersionDownload(String fileId, Integer version) {
+ try {
+ WpsDocVersionEntity versionEntity = wpsDocDomainService.getVersion(fileId, version);
+ Map data = new LinkedHashMap<>();
+ data.put("url", resolveDownloadUrl(versionEntity.getDownloadUrl(), versionEntity.getResourceId()));
+ return ok(data);
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public Map rename(String fileId, Map body) {
+ try {
+ String name = body == null || body.get("name") == null ? null : String.valueOf(body.get("name"));
+ wpsDocDomainService.rename(fileId, name);
+ return ok(Collections.emptyMap());
+ } catch (BizException e) {
+ return fail(e);
+ }
+ }
+
+ @Override
+ public WpsDocOpenCO myOrCreate() {
+ try {
+ Long userId = requireUserId();
+ String creatorId = String.valueOf(userId);
+ WpsDocEntity existing = wpsDocDomainService.findLatestByCreatorId(creatorId);
+ if (existing != null) {
+ return toOpenCO(existing, false);
+ }
+ WpsDocEntity created = doCreateBlankDoc(userId);
+ return toOpenCO(created, true);
+ } catch (Exception e) {
+ log.error("wps myOrCreate failed", e);
+ throw new BizException(ErrorCode.UNKNOWN_ERROR);
+ }
+ }
+
+ @Override
+ public WpsDocOpenCO createBlank() {
+ try {
+ Long userId = requireUserId();
+ WpsDocEntity created = doCreateBlankDoc(userId);
+ return toOpenCO(created, true);
+ } catch (Exception e) {
+ log.error("wps createBlank failed", e);
+ throw new BizException(ErrorCode.UNKNOWN_ERROR);
+ }
+ }
+
+ protected WpsDocEntity doCreateBlankDoc(Long userId) {
+ if (!StringUtils.hasText(defDocxUrl)) {
+ throw new BizException(ErrorCode.WPS_BLANK_DOCX_URL_EMPTY);
+ }
+ Long orgId = requireOrgId();
+ String creatorId = String.valueOf(userId);
+ String fileId = generateFileId();
+ String docName = "未命名文档.docx";
+ long nowEpoch = System.currentTimeMillis() / 1000;
+
+ FileStorage saved;
+ try (InputStream inputStream = new URL(defDocxUrl).openStream()) {
+ FileInfo ossFileInfo = fileStorageService.of(inputStream, docName).upload();
+ FileStorage fileStorage = FileStorageHelper.buildResource(ossFileInfo, docName, true);
+ saved = fileStorageDomainService.saveResource(fileStorage);
+ } catch (BizException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("download blank docx failed, url={}", defDocxUrl, e);
+ throw new BizException(ErrorCode.WPS_BLANK_DOCX_DOWNLOAD_FAILED, e);
+ }
+
+ WpsDocEntity doc = new WpsDocEntity();
+ doc.setFileId(fileId);
+ doc.setOrgId(orgId);
+ doc.setName(docName);
+ doc.setDocVersion(1);
+ doc.setSize(saved.getSize() == null ? 0L : saved.getSize());
+ doc.setResourceId(saved.getId());
+ doc.setDownloadUrl(saved.getUrl());
+ doc.setCreatorId(creatorId);
+ doc.setModifierId(creatorId);
+ doc.setCreateTimeEpoch(nowEpoch);
+ doc.setModifyTimeEpoch(nowEpoch);
+ doc.setTenantId(AuthUserContextAdapter.getCurrentTenantId());
+
+ WpsDocVersionEntity versionEntity = new WpsDocVersionEntity();
+ versionEntity.setOrgId(orgId);
+ versionEntity.setFileId(fileId);
+ versionEntity.setDocVersion(1);
+ versionEntity.setName(docName);
+ versionEntity.setSize(doc.getSize());
+ versionEntity.setResourceId(saved.getId());
+ versionEntity.setDownloadUrl(saved.getUrl());
+ versionEntity.setModifierId(creatorId);
+ versionEntity.setManualFlag(1);
+ versionEntity.setCreateTimeEpoch(nowEpoch);
+ versionEntity.setTenantId(doc.getTenantId());
+
+ return wpsDocDomainService.createInitial(doc, versionEntity);
+ }
+
+ private WpsDocOpenCO toOpenCO(WpsDocEntity doc, boolean created) {
+ WpsDocOpenCO co = new WpsDocOpenCO();
+ co.setFileId(doc.getFileId());
+ co.setName(doc.getName());
+ co.setCreated(created);
+ return co;
+ }
+
+ private String generateFileId() {
+ // 字母数字,不超过 47:wps + 32位uuid = 35
+ return "wps" + UUID.randomUUID().toString().replace("-", "");
+ }
+
+ private Long requireUserId() {
+ Long userId = AuthUserContextAdapter.getCurrentUserId();
+ if (userId == null) {
+ throw new BizException(ErrorCode.WPS_USER_NOT_LOGIN);
+ }
+ return userId;
+ }
+
+ private Long requireOrgId() {
+ Long orgId = AuthUserContextAdapter.getCurrentOrgId();
+ if (orgId != null) {
+ return orgId;
+ }
+ orgId = ThreadLocalUserInfoAdapter.get();
+ if (orgId != null) {
+ return orgId;
+ }
+ if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
+ orgId = ThreadLocalUserInfoAdapter.getOrgId();
+ }
+ if (orgId == null) {
+ throw new BizException(ErrorCode.ORG_INFO_NOT_FOUND);
+ }
+ return orgId;
+ }
+
+ private String resolveDownloadUrl(String downloadUrl, Integer resourceId) {
+ if (StringUtils.hasText(downloadUrl)) {
+ return downloadUrl;
+ }
+ if (resourceId == null) {
+ throw new BizException(ErrorCode.FILE_NOT_FOUND);
+ }
+ FileStorage resource = fileStorageDomainService.getResource(resourceId);
+ return resource.getUrl();
+ }
+
+ private Map ok(Object data) {
+ Map resp = new LinkedHashMap<>();
+ resp.put("code", 0);
+ resp.put("data", data);
+ return resp;
+ }
+
+ private Map fail(BizException e) {
+ Map resp = new LinkedHashMap<>();
+ resp.put("code", toWpsCode(e.getCode()));
+ resp.put("message", e.getMessage());
+ return resp;
+ }
+
+ private Map fail(ErrorCode errorCode) {
+ Map resp = new LinkedHashMap<>();
+ resp.put("code", toWpsCode(errorCode.getCode()));
+ resp.put("message", errorCode.getMessage());
+ return resp;
+ }
+
+ private int toWpsCode(String errorCode) {
+ if (ErrorCode.WPS_DOC_NOT_FOUND.getCode().equals(errorCode)
+ || ErrorCode.WPS_VERSION_NOT_FOUND.getCode().equals(errorCode)
+ || ErrorCode.FILE_NOT_FOUND.getCode().equals(errorCode)) {
+ return 40004;
+ }
+ if (ErrorCode.WPS_USER_NOT_LOGIN.getCode().equals(errorCode)) {
+ return 40005;
+ }
+ return 40001;
+ }
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/WpsApi.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/WpsApi.java
new file mode 100644
index 00000000..947b5750
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/WpsApi.java
@@ -0,0 +1,49 @@
+package org.qinan.safetyeval.client.api;
+
+import org.qinan.safetyeval.client.co.WpsDocOpenCO;
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * WPS WebOffice 回调接口(Client层)
+ *
+ * @author safety-eval
+ */
+public interface WpsApi {
+
+ Map getFileInfo(String fileId);
+
+ Map getDownload(String fileId);
+
+ Map getPermission(String fileId);
+
+ Map getUsers(List userIds);
+
+ Map uploadPrepare(String fileId);
+
+ Map uploadAddress(String fileId, Map body);
+
+ Map uploadComplete(String fileId, Map body);
+
+ Map uploadBinary(String fileId, InputStream inputStream);
+
+ Map listVersions(String fileId);
+
+ Map getVersion(String fileId, Integer version);
+
+ Map getVersionDownload(String fileId, Integer version);
+
+ Map rename(String fileId, Map body);
+
+ /**
+ * 打开页:当前用户有文档则返回最近一份,没有则创建空白 docx
+ */
+ WpsDocOpenCO myOrCreate();
+
+ /**
+ * 始终新建一份空白 docx(即使用户已有文档)
+ */
+ WpsDocOpenCO createBlank();
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsDocOpenCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsDocOpenCO.java
new file mode 100644
index 00000000..ee1a7650
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsDocOpenCO.java
@@ -0,0 +1,23 @@
+package org.qinan.safetyeval.client.co;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 打开/新建 WPS 文档返回
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsDocOpenCO {
+
+ /** WPS file_id,唯一 */
+ @ApiModelProperty(value = "WPS file_id,唯一")
+ private String fileId;
+ /** 文档名称 */
+ @ApiModelProperty(value = "文档名称")
+ private String name;
+ /** 是否本次新建 */
+ @ApiModelProperty(value = "是否本次新建")
+ private Boolean created;
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsFileCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsFileCO.java
new file mode 100644
index 00000000..e6b45ea3
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsFileCO.java
@@ -0,0 +1,30 @@
+package org.qinan.safetyeval.client.co;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * WPS 文件信息 CO(官方字段 snake_case)
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsFileCO {
+
+ private String id;
+ private String name;
+ private Integer version;
+ private Long size;
+
+ @JsonProperty("create_time")
+ private Long createTime;
+
+ @JsonProperty("modify_time")
+ private Long modifyTime;
+
+ @JsonProperty("creator_id")
+ private String creatorId;
+
+ @JsonProperty("modifier_id")
+ private String modifierId;
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsPermissionCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsPermissionCO.java
new file mode 100644
index 00000000..8792930f
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsPermissionCO.java
@@ -0,0 +1,26 @@
+package org.qinan.safetyeval.client.co;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * WPS 文档权限 CO(与官方 permission 回调字段一致)
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsPermissionCO {
+
+ @JsonProperty("user_id")
+ private String userId;
+
+ private Integer read;
+ private Integer update;
+ private Integer download;
+ private Integer rename;
+ private Integer history;
+ private Integer copy;
+ private Integer print;
+ private Integer saveas;
+ private Integer comment;
+}
diff --git a/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsUserCO.java b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsUserCO.java
new file mode 100644
index 00000000..3c80aee9
--- /dev/null
+++ b/safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsUserCO.java
@@ -0,0 +1,19 @@
+package org.qinan.safetyeval.client.co;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * WPS 用户信息 CO
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsUserCO {
+
+ private String id;
+ private String name;
+
+ @JsonProperty("avatar_url")
+ private String avatarUrl;
+}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocEntity.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocEntity.java
new file mode 100644
index 00000000..98478d9a
--- /dev/null
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocEntity.java
@@ -0,0 +1,33 @@
+package org.qinan.safetyeval.domain.entity;
+
+import lombok.Data;
+
+/**
+ * WPS 在线文档领域实体
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsDocEntity {
+
+ private Long id;
+ private String fileId;
+ private Long orgId;
+ private String name;
+ private Integer docVersion;
+ private Long size;
+ private Integer resourceId;
+ private String downloadUrl;
+ private String creatorId;
+ private String modifierId;
+ private Long createTimeEpoch;
+ private Long modifyTimeEpoch;
+ private Long tenantId;
+
+ /** 三阶段上传待确认 filestorage.id */
+ private Integer pendingResourceId;
+ /** 三阶段上传待确认 URL */
+ private String pendingDownloadUrl;
+ /** 三阶段上传待确认大小 */
+ private Long pendingSize;
+}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocVersionEntity.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocVersionEntity.java
new file mode 100644
index 00000000..361d8a8f
--- /dev/null
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocVersionEntity.java
@@ -0,0 +1,27 @@
+package org.qinan.safetyeval.domain.entity;
+
+import lombok.Data;
+
+/**
+ * WPS 文档版本领域实体
+ *
+ * @author safety-eval
+ */
+@Data
+public class WpsDocVersionEntity {
+
+ private Long id;
+ private Long orgId;
+ private String fileId;
+ private Integer docVersion;
+ private String name;
+ private Long size;
+ private Integer resourceId;
+ private String downloadUrl;
+ private String digestSha1;
+ private String modifierId;
+ /** 1手动保存 2自动保存 */
+ private Integer manualFlag;
+ private Long createTimeEpoch;
+ private Long tenantId;
+}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
index e04f18d5..29b06ea5 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/exception/ErrorCode.java
@@ -94,6 +94,15 @@ public enum ErrorCode {
FILE_URL_GENERATE_FAILED("03-01-007", "文件地址生成失败"),
FILE_SIZE_EXCEEDED("03-01-008", "文件大小超出限制"),
+ // ---- WPS ----
+ WPS_DOC_NOT_FOUND("05-01-001", "WPS文档不存在"),
+ WPS_USER_NOT_LOGIN("05-01-002", "WPS用户未登录"),
+ WPS_UPLOAD_PENDING_NOT_FOUND("05-01-003", "WPS上传结果不存在"),
+ WPS_VERSION_NOT_FOUND("05-01-004", "WPS文档版本不存在"),
+ WPS_DOC_NAME_INVALID("05-01-005", "WPS文档名称不合法"),
+ WPS_BLANK_DOCX_URL_EMPTY("05-01-006", "WPS空白模板地址未配置"),
+ WPS_BLANK_DOCX_DOWNLOAD_FAILED("05-01-007", "WPS空白模板下载失败"),
+
/**
* 短信码不存在
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/AccountGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/AccountGateway.java
index 564727c7..cd4cec76 100644
--- a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/AccountGateway.java
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/AccountGateway.java
@@ -4,6 +4,8 @@ import org.qinan.safetyeval.domain.entity.AccountEntity;
import org.qinan.safetyeval.domain.query.AccountQuery;
import org.qinan.safetyeval.domain.query.PageResult;
+import java.util.List;
+
/**
* 用户Gateway接口(Domain层定义)
*
@@ -24,4 +26,6 @@ public interface AccountGateway {
PageResult page(AccountQuery query);
AccountEntity foundByPhone(String phone);
+
+ List listByIds(List ids);
}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocGateway.java
new file mode 100644
index 00000000..e07f85a8
--- /dev/null
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocGateway.java
@@ -0,0 +1,27 @@
+package org.qinan.safetyeval.domain.gateway;
+
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+
+/**
+ * WPS 文档 Gateway
+ *
+ * @author safety-eval
+ */
+public interface WpsDocGateway {
+
+ WpsDocEntity getByFileId(String fileId);
+
+ /**
+ * 当前用户最近一份文档(按 id 倒序)
+ */
+ WpsDocEntity getLatestByCreatorId(String creatorId);
+
+ WpsDocEntity save(WpsDocEntity entity);
+
+ WpsDocEntity modify(WpsDocEntity entity);
+
+ /**
+ * 清空三阶段上传 pending 字段(需显式写 null)
+ */
+ void clearPending(Long id);
+}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocVersionGateway.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocVersionGateway.java
new file mode 100644
index 00000000..c590f990
--- /dev/null
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocVersionGateway.java
@@ -0,0 +1,19 @@
+package org.qinan.safetyeval.domain.gateway;
+
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+
+import java.util.List;
+
+/**
+ * WPS 文档版本 Gateway
+ *
+ * @author safety-eval
+ */
+public interface WpsDocVersionGateway {
+
+ WpsDocVersionEntity save(WpsDocVersionEntity entity);
+
+ WpsDocVersionEntity getByFileIdAndVersion(String fileId, Integer docVersion);
+
+ List listByFileIdDesc(String fileId);
+}
diff --git a/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/service/WpsDocDomainService.java b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/service/WpsDocDomainService.java
new file mode 100644
index 00000000..63634c14
--- /dev/null
+++ b/safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/service/WpsDocDomainService.java
@@ -0,0 +1,108 @@
+package org.qinan.safetyeval.domain.service;
+
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+import org.qinan.safetyeval.domain.exception.BizException;
+import org.qinan.safetyeval.domain.exception.ErrorCode;
+import org.qinan.safetyeval.domain.gateway.WpsDocGateway;
+import org.qinan.safetyeval.domain.gateway.WpsDocVersionGateway;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * WPS 文档领域服务
+ *
+ * @author safety-eval
+ */
+@Service
+public class WpsDocDomainService {
+
+ private static final Pattern INVALID_NAME_CHARS = Pattern.compile("[\\\\/|:\"*?<>]");
+
+ @Resource
+ private WpsDocGateway wpsDocGateway;
+ @Resource
+ private WpsDocVersionGateway wpsDocVersionGateway;
+
+ public WpsDocEntity getByFileId(String fileId) {
+ WpsDocEntity entity = wpsDocGateway.getByFileId(fileId);
+ if (entity == null) {
+ throw new BizException(ErrorCode.WPS_DOC_NOT_FOUND);
+ }
+ return entity;
+ }
+
+ /**
+ * 当前用户最近一份文档,没有则返回 null
+ */
+ public WpsDocEntity findLatestByCreatorId(String creatorId) {
+ return wpsDocGateway.getLatestByCreatorId(creatorId);
+ }
+
+ /**
+ * 初始化文档主表 + 首个版本
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public WpsDocEntity createInitial(WpsDocEntity doc, WpsDocVersionEntity versionEntity) {
+ WpsDocEntity saved = wpsDocGateway.save(doc);
+ versionEntity.setFileId(saved.getFileId());
+ versionEntity.setOrgId(saved.getOrgId());
+ versionEntity.setTenantId(saved.getTenantId());
+ wpsDocVersionGateway.save(versionEntity);
+ return saved;
+ }
+
+ public List listVersions(String fileId) {
+ getByFileId(fileId);
+ return wpsDocVersionGateway.listByFileIdDesc(fileId);
+ }
+
+ public WpsDocVersionEntity getVersion(String fileId, Integer docVersion) {
+ getByFileId(fileId);
+ WpsDocVersionEntity version = wpsDocVersionGateway.getByFileIdAndVersion(fileId, docVersion);
+ if (version == null) {
+ throw new BizException(ErrorCode.WPS_VERSION_NOT_FOUND);
+ }
+ return version;
+ }
+
+ /**
+ * 三阶段 PUT 上传成功后写入 pending
+ */
+ public WpsDocEntity savePendingUpload(String fileId, Integer resourceId, String downloadUrl, Long size) {
+ WpsDocEntity doc = getByFileId(fileId);
+ doc.setPendingResourceId(resourceId);
+ doc.setPendingDownloadUrl(downloadUrl);
+ doc.setPendingSize(size);
+ return wpsDocGateway.modify(doc);
+ }
+
+ /**
+ * 保存完成后更新主表、写入版本记录,并清空 pending
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public WpsDocEntity completeSave(WpsDocEntity latest, WpsDocVersionEntity versionEntity) {
+ WpsDocEntity updated = wpsDocGateway.modify(latest);
+ wpsDocVersionGateway.save(versionEntity);
+ wpsDocGateway.clearPending(updated.getId());
+ return wpsDocGateway.getByFileId(updated.getFileId());
+ }
+
+ /**
+ * 重命名文档
+ */
+ public WpsDocEntity rename(String fileId, String name) {
+ if (!StringUtils.hasText(name) || name.length() > 240 || INVALID_NAME_CHARS.matcher(name).find()) {
+ throw new BizException(ErrorCode.WPS_DOC_NAME_INVALID);
+ }
+ WpsDocEntity doc = getByFileId(fileId);
+ doc.setName(name.trim());
+ doc.setModifyTimeEpoch(System.currentTimeMillis() / 1000);
+ return wpsDocGateway.modify(doc);
+ }
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocDoConvertor.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocDoConvertor.java
new file mode 100644
index 00000000..e693cc0e
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocDoConvertor.java
@@ -0,0 +1,18 @@
+package org.qinan.safetyeval.infrastructure.convertor;
+
+import org.mapstruct.Mapper;
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocDO;
+
+/**
+ * WPS 文档 Entity ↔ DO
+ *
+ * @author safety-eval
+ */
+@Mapper(componentModel = "spring")
+public interface WpsDocDoConvertor {
+
+ WpsDocDO toDO(WpsDocEntity entity);
+
+ WpsDocEntity toEntity(WpsDocDO dataObject);
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocVersionDoConvertor.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocVersionDoConvertor.java
new file mode 100644
index 00000000..b99a7ac9
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocVersionDoConvertor.java
@@ -0,0 +1,22 @@
+package org.qinan.safetyeval.infrastructure.convertor;
+
+import org.mapstruct.Mapper;
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocVersionDO;
+
+import java.util.List;
+
+/**
+ * WPS 文档版本 Entity ↔ DO
+ *
+ * @author safety-eval
+ */
+@Mapper(componentModel = "spring")
+public interface WpsDocVersionDoConvertor {
+
+ WpsDocVersionDO toDO(WpsDocVersionEntity entity);
+
+ WpsDocVersionEntity toEntity(WpsDocVersionDO dataObject);
+
+ List toEntityList(List dataObjects);
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocDO.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocDO.java
new file mode 100644
index 00000000..15ad1e27
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocDO.java
@@ -0,0 +1,47 @@
+package org.qinan.safetyeval.infrastructure.dataobject;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * WPS 文档 DO
+ *
+ * @author safety-eval
+ */
+@Data
+@TableName("wps_doc")
+public class WpsDocDO {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String fileId;
+ private Long orgId;
+ private String name;
+ private Integer docVersion;
+ private Long size;
+ private Integer resourceId;
+ private String downloadUrl;
+ private String creatorId;
+ private String modifierId;
+ private Long createTimeEpoch;
+ private Long modifyTimeEpoch;
+ private String deleteEnum;
+ private String remarks;
+ private String createName;
+ private String updateName;
+ private Long tenantId;
+ private Integer version;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+ private Long createId;
+ private Long updateId;
+ private String env;
+
+ private Integer pendingResourceId;
+ private String pendingDownloadUrl;
+ private Long pendingSize;
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocVersionDO.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocVersionDO.java
new file mode 100644
index 00000000..c4552d0c
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocVersionDO.java
@@ -0,0 +1,36 @@
+package org.qinan.safetyeval.infrastructure.dataobject;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * WPS 文档版本 DO
+ *
+ * @author safety-eval
+ */
+@Data
+@TableName("wps_doc_version")
+public class WpsDocVersionDO {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long orgId;
+ private String fileId;
+ private Integer docVersion;
+ private String name;
+ private Long size;
+ private Integer resourceId;
+ private String downloadUrl;
+ private String digestSha1;
+ private String modifierId;
+ private Integer manualFlag;
+ private Long createTimeEpoch;
+ private String deleteEnum;
+ private Long tenantId;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/AccountGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/AccountGatewayImpl.java
index 8c4ea766..c02668a6 100644
--- a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/AccountGatewayImpl.java
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/AccountGatewayImpl.java
@@ -109,6 +109,19 @@ public class AccountGatewayImpl implements AccountGateway {
return toEntity(dataObject);
}
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return java.util.Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.in(AccountDO::getId, ids);
+ wrapper.eq(AccountDO::getDeleteEnum, "false");
+ return accountMapper.selectList(wrapper).stream()
+ .map(this::toEntity)
+ .collect(Collectors.toList());
+ }
+
private AccountDO toDO(AccountEntity entity) {
AccountDO dataObject = new AccountDO();
dataObject.setUserName(entity.getUserName());
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocGatewayImpl.java
new file mode 100644
index 00000000..e143236b
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocGatewayImpl.java
@@ -0,0 +1,74 @@
+package org.qinan.safetyeval.infrastructure.gatewayimpl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.qinan.safetyeval.domain.entity.WpsDocEntity;
+import org.qinan.safetyeval.domain.gateway.WpsDocGateway;
+import org.qinan.safetyeval.infrastructure.convertor.WpsDocDoConvertor;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocDO;
+import org.qinan.safetyeval.infrastructure.mapper.WpsDocMapper;
+import org.qinan.safetyeval.infrastructure.support.InsertFieldDefaults;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+
+/**
+ * WPS 文档 Gateway 实现
+ *
+ * @author safety-eval
+ */
+@Component
+public class WpsDocGatewayImpl implements WpsDocGateway {
+
+ @Resource
+ private WpsDocMapper wpsDocMapper;
+ @Resource
+ private WpsDocDoConvertor wpsDocDoConvertor;
+
+ @Override
+ public WpsDocEntity getByFileId(String fileId) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(WpsDocDO::getFileId, fileId);
+ wrapper.eq(WpsDocDO::getDeleteEnum, "false");
+ return wpsDocDoConvertor.toEntity(wpsDocMapper.selectOne(wrapper));
+ }
+
+ @Override
+ public WpsDocEntity getLatestByCreatorId(String creatorId) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(WpsDocDO::getCreatorId, creatorId);
+ wrapper.eq(WpsDocDO::getDeleteEnum, "false");
+ wrapper.orderByDesc(WpsDocDO::getId);
+ wrapper.last("limit 1");
+ return wpsDocDoConvertor.toEntity(wpsDocMapper.selectOne(wrapper));
+ }
+
+ @Override
+ public WpsDocEntity save(WpsDocEntity entity) {
+ WpsDocDO dataObject = wpsDocDoConvertor.toDO(entity);
+ InsertFieldDefaults.apply(dataObject);
+ wpsDocMapper.insert(dataObject);
+ return wpsDocDoConvertor.toEntity(dataObject);
+ }
+
+ @Override
+ public WpsDocEntity modify(WpsDocEntity entity) {
+ WpsDocDO dataObject = wpsDocDoConvertor.toDO(entity);
+ dataObject.setId(entity.getId());
+ InsertFieldDefaults.applyForUpdate(dataObject);
+ wpsDocMapper.updateById(dataObject);
+ return getByFileId(entity.getFileId());
+ }
+
+ @Override
+ public void clearPending(Long id) {
+ LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
+ wrapper.eq(WpsDocDO::getId, id)
+ .set(WpsDocDO::getPendingResourceId, null)
+ .set(WpsDocDO::getPendingDownloadUrl, null)
+ .set(WpsDocDO::getPendingSize, null)
+ .set(WpsDocDO::getUpdateTime, LocalDateTime.now());
+ wpsDocMapper.update(null, wrapper);
+ }
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocVersionGatewayImpl.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocVersionGatewayImpl.java
new file mode 100644
index 00000000..5ac83aab
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocVersionGatewayImpl.java
@@ -0,0 +1,62 @@
+package org.qinan.safetyeval.infrastructure.gatewayimpl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import org.qinan.safetyeval.domain.entity.WpsDocVersionEntity;
+import org.qinan.safetyeval.domain.gateway.WpsDocVersionGateway;
+import org.qinan.safetyeval.infrastructure.convertor.WpsDocVersionDoConvertor;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocVersionDO;
+import org.qinan.safetyeval.infrastructure.mapper.WpsDocVersionMapper;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * WPS 文档版本 Gateway 实现
+ *
+ * @author safety-eval
+ */
+@Component
+public class WpsDocVersionGatewayImpl implements WpsDocVersionGateway {
+
+ @Resource
+ private WpsDocVersionMapper wpsDocVersionMapper;
+ @Resource
+ private WpsDocVersionDoConvertor wpsDocVersionDoConvertor;
+
+ @Override
+ public WpsDocVersionEntity save(WpsDocVersionEntity entity) {
+ WpsDocVersionDO dataObject = wpsDocVersionDoConvertor.toDO(entity);
+ if (dataObject.getDeleteEnum() == null) {
+ dataObject.setDeleteEnum("false");
+ }
+ LocalDateTime now = LocalDateTime.now();
+ if (dataObject.getCreateTime() == null) {
+ dataObject.setCreateTime(now);
+ }
+ if (dataObject.getUpdateTime() == null) {
+ dataObject.setUpdateTime(now);
+ }
+ wpsDocVersionMapper.insert(dataObject);
+ return wpsDocVersionDoConvertor.toEntity(dataObject);
+ }
+
+ @Override
+ public WpsDocVersionEntity getByFileIdAndVersion(String fileId, Integer docVersion) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(WpsDocVersionDO::getFileId, fileId);
+ wrapper.eq(WpsDocVersionDO::getDocVersion, docVersion);
+ wrapper.eq(WpsDocVersionDO::getDeleteEnum, "false");
+ return wpsDocVersionDoConvertor.toEntity(wpsDocVersionMapper.selectOne(wrapper));
+ }
+
+ @Override
+ public List listByFileIdDesc(String fileId) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(WpsDocVersionDO::getFileId, fileId);
+ wrapper.eq(WpsDocVersionDO::getDeleteEnum, "false");
+ wrapper.orderByDesc(WpsDocVersionDO::getDocVersion);
+ return wpsDocVersionDoConvertor.toEntityList(wpsDocVersionMapper.selectList(wrapper));
+ }
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocMapper.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocMapper.java
new file mode 100644
index 00000000..d4d70a1b
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocMapper.java
@@ -0,0 +1,14 @@
+package org.qinan.safetyeval.infrastructure.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocDO;
+
+/**
+ * WPS 文档 Mapper
+ *
+ * @author safety-eval
+ */
+@Mapper
+public interface WpsDocMapper extends BaseMapper {
+}
diff --git a/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocVersionMapper.java b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocVersionMapper.java
new file mode 100644
index 00000000..1e6c02ba
--- /dev/null
+++ b/safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocVersionMapper.java
@@ -0,0 +1,14 @@
+package org.qinan.safetyeval.infrastructure.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.qinan.safetyeval.infrastructure.dataobject.WpsDocVersionDO;
+
+/**
+ * WPS 文档版本 Mapper
+ *
+ * @author safety-eval
+ */
+@Mapper
+public interface WpsDocVersionMapper extends BaseMapper {
+}
diff --git a/safety-eval-start/src/main/resources/nacos/config-spring.yml b/safety-eval-start/src/main/resources/nacos/config-spring.yml
index 3b3aac81..587fc966 100644
--- a/safety-eval-start/src/main/resources/nacos/config-spring.yml
+++ b/safety-eval-start/src/main/resources/nacos/config-spring.yml
@@ -171,3 +171,7 @@ def:
password: a123456
publicKey: 0402df2195296d4062ac85ad766994d73e871b887e18efb9a9a06b4cebc72372869b7da6c347c129dee2b46a0f279ff066b01c76208c2a052af75977c722a2ccee
reviewEnable: false
+
+wps:
+ upload-base-url: ${WPS_UPLOAD_BASE_URL:https://gbs-gateway.qhdsafety.com/safetyEval-h5/wsp}
+ def-docx-url: https://test-dragon-yf-pub.oss-cn-hangzhou.aliyuncs.com/jjb/6a59d83ee4b0b4541389511f.docx