From 3aabb226e9488839fb6a0ba991af4bc4327fe6c7 Mon Sep 17 00:00:00 2001 From: zhanglei Date: Mon, 20 Jul 2026 14:45:19 +0800 Subject: [PATCH] feat: wps --- .../adapter/web/H5OpenController.java | 192 ++++--- .../app/convertor/WpsCmdConvertor.java | 36 ++ .../safetyeval/app/executor/WpsExecutor.java | 480 ++++++++++++++++++ .../qinan/safetyeval/client/api/WpsApi.java | 49 ++ .../safetyeval/client/co/WpsDocOpenCO.java | 23 + .../qinan/safetyeval/client/co/WpsFileCO.java | 30 ++ .../safetyeval/client/co/WpsPermissionCO.java | 26 + .../qinan/safetyeval/client/co/WpsUserCO.java | 19 + .../domain/entity/WpsDocEntity.java | 33 ++ .../domain/entity/WpsDocVersionEntity.java | 27 + .../domain/exception/ErrorCode.java | 9 + .../domain/gateway/AccountGateway.java | 4 + .../domain/gateway/WpsDocGateway.java | 27 + .../domain/gateway/WpsDocVersionGateway.java | 19 + .../domain/service/WpsDocDomainService.java | 108 ++++ .../convertor/WpsDocDoConvertor.java | 18 + .../convertor/WpsDocVersionDoConvertor.java | 22 + .../infrastructure/dataobject/WpsDocDO.java | 47 ++ .../dataobject/WpsDocVersionDO.java | 36 ++ .../gatewayimpl/AccountGatewayImpl.java | 13 + .../gatewayimpl/WpsDocGatewayImpl.java | 74 +++ .../gatewayimpl/WpsDocVersionGatewayImpl.java | 62 +++ .../infrastructure/mapper/WpsDocMapper.java | 14 + .../mapper/WpsDocVersionMapper.java | 14 + .../main/resources/nacos/config-spring.yml | 4 + 25 files changed, 1306 insertions(+), 80 deletions(-) create mode 100644 safety-eval-app/src/main/java/org/qinan/safetyeval/app/convertor/WpsCmdConvertor.java create mode 100644 safety-eval-app/src/main/java/org/qinan/safetyeval/app/executor/WpsExecutor.java create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/api/WpsApi.java create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsDocOpenCO.java create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsFileCO.java create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsPermissionCO.java create mode 100644 safety-eval-client/src/main/java/org/qinan/safetyeval/client/co/WpsUserCO.java create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocEntity.java create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/entity/WpsDocVersionEntity.java create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocGateway.java create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/gateway/WpsDocVersionGateway.java create mode 100644 safety-eval-domain/src/main/java/org/qinan/safetyeval/domain/service/WpsDocDomainService.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocDoConvertor.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/convertor/WpsDocVersionDoConvertor.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocDO.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/dataobject/WpsDocVersionDO.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocGatewayImpl.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/gatewayimpl/WpsDocVersionGatewayImpl.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocMapper.java create mode 100644 safety-eval-infrastructure/src/main/java/org/qinan/safetyeval/infrastructure/mapper/WpsDocVersionMapper.java 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