From 43aab58c9ecb1203770ba382efbdbae299ca87ec Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期一, 26 十月 2026 15:12:31 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_business' into dev_business

---
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java |  454 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 454 insertions(+), 0 deletions(-)

diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java
new file mode 100644
index 0000000..31c73b1
--- /dev/null
+++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/agent/imports/SystemAgentGenericImportService.java
@@ -0,0 +1,454 @@
+package cn.iocoder.yudao.module.system.service.agent.imports;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.yudao.module.system.api.agent.imports.*;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.agent.AgentImportCommandRowDO;
+import cn.iocoder.yudao.module.system.dal.mysql.agent.AgentImportCommandMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.agent.AgentImportCommandRowMapper;
+import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException;
+import cn.iocoder.yudao.module.system.framework.agent.AgentPrincipal;
+import cn.iocoder.yudao.module.system.service.storage.SystemAgentImportBridgeService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.validation.annotation.Validated;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 閫氱敤涓氬姟瀵煎叆缂栨帓锛氶�傞厤鍣ㄥ彧璐熻矗鍗曡鏍¢獙/鍐欏叆锛屾湰绫荤粺涓�澶勭悊鏉冮檺銆佸箓绛夈�侀�愯浜嬪姟涓庡洖鎵с��
+ */
+@Service
+@Validated
+public class SystemAgentGenericImportService {
+
+    public static final String STATUS_RUNNING = "RUNNING";
+    public static final String STATUS_SUCCEEDED = "SUCCEEDED";
+    public static final String STATUS_PARTIAL_FAILED = "PARTIAL_FAILED";
+    public static final String STATUS_FAILED = "FAILED";
+    public static final String STATUS_UNKNOWN = "UNKNOWN";
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final Duration LEASE_DURATION = Duration.ofMinutes(5);
+
+    @Resource
+    private ObjectProvider<AgentImportModuleAdapter> adapterProvider;
+    @Resource
+    private SecurityFrameworkService securityFrameworkService;
+    @Resource
+    private SystemAgentImportBridgeService attachmentService;
+    @Resource
+    private AgentImportCommandMapper commandMapper;
+    @Resource
+    private AgentImportCommandRowMapper commandRowMapper;
+    @Resource
+    private PlatformTransactionManager transactionManager;
+
+    @Value("${yudao.agent.bridge.write-enabled:false}")
+    private boolean writeEnabled;
+
+    @Value("${yudao.agent.bridge.max-rows:5000}")
+    private int maxRows;
+
+    public List<Map<String, String>> modules() {
+        return adapters().values().stream()
+                .map(adapter -> Map.of("moduleId", adapter.moduleId(), "permission", adapter.permission()))
+                .toList();
+    }
+
+    public AgentImportPreflightRespVO preflight(String moduleId, AgentImportPreflightReqVO request,
+                                                AgentPrincipal principal) {
+        AgentImportModuleAdapter adapter = requireAdapter(moduleId);
+        requirePermission(adapter);
+        validateAttachment(request.getAttachmentId(), request.getSourceSha256());
+        validateRows(request);
+        AgentImportPreflightRespVO result = adapter.preflight(principal, request);
+        validatePlan(result, request);
+        return result;
+    }
+
+    public AgentImportReceiptRespVO commit(String moduleId, AgentImportCommitReqVO request,
+                                           AgentPrincipal principal) {
+        if (!writeEnabled) {
+            throw new AgentBridgeException(HttpStatus.SERVICE_UNAVAILABLE, "WRITE_DISABLED",
+                    "鏅鸿兘浣撳啓鍏ュ紑鍏虫湭鎵撳紑锛屾彁浜ゅ凡鎷掔粷");
+        }
+        AgentImportModuleAdapter adapter = requireAdapter(moduleId);
+        requirePermission(adapter);
+        validateAttachment(request.getAttachmentId(), request.getSourceSha256());
+        validateRows(request);
+
+        Long ownerUserId = principal.userId();
+        String digest = requestDigest(moduleId, request);
+
+        // 1. 骞傜瓑鍛戒腑锛氬懡浠� ID 浼樺厛锛屽叾娆″箓绛夐敭
+        AgentImportCommandDO existing = commandMapper.selectByCommandId(
+                ownerUserId, moduleId, request.getCommandId());
+        if (existing != null) {
+            requireSameDigest(existing, digest);
+            return toReceipt(existing);
+        }
+        existing = commandMapper.selectByIdempotencyKey(
+                ownerUserId, moduleId, request.getIdempotencyKey());
+        if (existing != null) {
+            requireSameDigest(existing, digest);
+            return toReceipt(existing);
+        }
+
+        // 2. 閲嶆柊棰勬骞舵瘮瀵硅鍒掓憳瑕侊紝闃叉纭鍚庝富鏁版嵁鍙樺寲
+        AgentImportPreflightRespVO plan = adapter.preflight(principal, request);
+        validatePlan(plan, request);
+        if (!StrUtil.equalsIgnoreCase(request.getPreflightHash(), plan.getPreflightHash())) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "PLAN_STALE",
+                    "棰勬璁″垝宸插彉鍖栵紝璇烽噸鏂伴妫�骞剁‘璁�", false, Map.of("preflight", plan));
+        }
+        if (!Boolean.TRUE.equals(plan.getCanCommit())) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "PREFLIGHT_BLOCKED",
+                    "棰勬瀛樺湪闃绘柇椤癸紝涓嶈兘鎻愪氦", false, Map.of("preflight", plan));
+        }
+
+        // 3. 鐧昏鍛戒护锛堝敮涓�閿厹浣忓苟鍙戦噸澶嶆彁浜わ級
+        AgentImportCommandDO command = AgentImportCommandDO.builder()
+                .commandId(request.getCommandId())
+                .idempotencyKey(request.getIdempotencyKey())
+                .requestDigest(digest)
+                .moduleId(moduleId)
+                .ownerUserId(ownerUserId)
+                .sessionId(request.getSessionId())
+                .attachmentId(request.getAttachmentId())
+                .sourceSha256(request.getSourceSha256().toLowerCase(Locale.ROOT))
+                .preflightHash(request.getPreflightHash().toLowerCase(Locale.ROOT))
+                .proposalHash(request.getProposalHash())
+                .status(STATUS_RUNNING)
+                .totalCount(request.getRows().size())
+                .successCount(0).failedCount(0).skippedCount(0)
+                .leaseExpiresAt(LocalDateTime.now().plus(LEASE_DURATION))
+                .build();
+        try {
+            commandMapper.insert(command);
+        } catch (DuplicateKeyException ex) {
+            AgentImportCommandDO raced = commandMapper.selectByCommandId(
+                    ownerUserId, moduleId, request.getCommandId());
+            if (raced == null) {
+                raced = commandMapper.selectByIdempotencyKey(
+                        ownerUserId, moduleId, request.getIdempotencyKey());
+            }
+            if (raced == null) {
+                throw ex;
+            }
+            requireSameDigest(raced, digest);
+            return toReceipt(raced);
+        }
+
+        // 4. 閫愯鐙珛浜嬪姟鎵ц锛涘崟琛屽け璐ヤ笉褰卞搷鍏朵粬琛�
+        int success = 0;
+        int failed = 0;
+        int skipped = 0;
+        TransactionTemplate rowTx = new TransactionTemplate(transactionManager);
+        try {
+            for (int index = 0; index < request.getRows().size(); index++) {
+                AgentImportPreflightReqVO.Row row = request.getRows().get(index);
+                AgentImportPreflightRespVO.Row planned = plan.getRows().get(index);
+                if ("SKIP".equals(planned.getAction())) {
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action("SKIP").status("SKIPPED")
+                            .targetItemId(planned.getTargetId()).retryable(false).build());
+                    skipped++;
+                    continue;
+                }
+                if (!"CREATE".equals(planned.getAction())) {
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action("CREATE").status("FAILED")
+                            .errorCode("PREFLIGHT_BLOCKED").errorMessage("璇ヨ棰勬鏈�氳繃").retryable(false).build());
+                    failed++;
+                    continue;
+                }
+                try {
+                    AgentImportRowReceipt receipt = rowTx.execute(status ->
+                            adapter.commitRow(principal, request, planned, row));
+                    if (receipt == null) {
+                        throw new IllegalStateException("閫傞厤鍣ㄦ湭杩斿洖鍥炴墽");
+                    }
+                    commandRowMapper.insert(baseRow(moduleId, request.getCommandId(), row)
+                            .action(StrUtil.blankToDefault(receipt.action(), "CREATE"))
+                            .status("SUCCESS")
+                            .targetItemId(receipt.targetId())
+                            .targetVersion(receipt.targetVersion())
+                            .code(StrUtil.blankToDefault(receipt.code(), row.getValues().get("code")))
+                            .name(StrUtil.blankToDefault(receipt.name(), row.getValues().get("name")))
+                            .retryable(false).build());
+                    success++;
+                } catch (Exception ex) {
+                    recordFailure(moduleId, request.getCommandId(), row, ex);
+                    failed++;
+                }
+            }
+        } catch (Exception ex) {
+            command.setStatus(STATUS_UNKNOWN);
+            command.setErrorCode("COMMIT_INTERRUPTED");
+            command.setErrorMessage(trim(ex.getMessage(), 512));
+            command.setSuccessCount(success);
+            command.setFailedCount(failed);
+            command.setSkippedCount(skipped);
+            command.setUpdateTime(LocalDateTime.now());
+            commandMapper.updateById(command);
+            return toReceipt(command);
+        }
+
+        command.setSuccessCount(success);
+        command.setFailedCount(failed);
+        command.setSkippedCount(skipped);
+        command.setStatus(failed == 0 ? STATUS_SUCCEEDED
+                : (success + skipped == 0 ? STATUS_FAILED : STATUS_PARTIAL_FAILED));
+        command.setLeaseExpiresAt(LocalDateTime.now());
+        command.setUpdateTime(LocalDateTime.now());
+        commandMapper.updateById(command);
+        return toReceipt(command);
+    }
+
+    public AgentImportReceiptRespVO getCommand(String moduleId, String commandId, AgentPrincipal principal) {
+        requireAdapter(moduleId);
+        AgentImportCommandDO command = commandMapper.selectByCommandId(
+                principal.userId(), moduleId, commandId);
+        if (command == null) {
+            throw new AgentBridgeException(HttpStatus.NOT_FOUND, "COMMAND_NOT_FOUND", "瀵煎叆鍛戒护涓嶅瓨鍦�");
+        }
+        return toReceipt(command);
+    }
+
+    private void recordFailure(String moduleId, String commandId, AgentImportPreflightReqVO.Row row,
+                               Exception ex) {
+        String errorCode = "ROW_WRITE_FAILED";
+        boolean retryable = true;
+        String message = ex.getMessage();
+        if (ex instanceof AgentBridgeException bridge) {
+            errorCode = bridge.getErrorCode();
+            retryable = bridge.isRetryable();
+            message = bridge.getReason();
+        } else if (ex instanceof ServiceException service) {
+            errorCode = "ROW_WRITE_REJECTED";
+            retryable = false;
+            message = service.getMessage();
+        }
+        commandRowMapper.insert(baseRow(moduleId, commandId, row)
+                .action("CREATE").status("FAILED")
+                .errorCode(errorCode).errorMessage(trim(message, 512)).retryable(retryable).build());
+    }
+
+    private AgentImportPreflightRespVO.Row requirePlanRow(AgentImportPreflightRespVO plan, int index) {
+        if (plan.getRows() == null || index >= plan.getRows().size()) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛屾暟涓庤姹備笉涓�鑷�");
+        }
+        return plan.getRows().get(index);
+    }
+
+    private void validatePlan(AgentImportPreflightRespVO plan, AgentImportPreflightReqVO request) {
+        if (plan == null || StrUtil.isBlank(plan.getPreflightHash())) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄦ湭杩斿洖棰勬鎽樿");
+        }
+        if (plan.getRows() == null || plan.getRows().size() != request.getRows().size()) {
+            throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                    "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛屾暟涓庤姹備笉涓�鑷�");
+        }
+        for (int index = 0; index < plan.getRows().size(); index++) {
+            if (!request.getRows().get(index).getCandidateId()
+                    .equals(plan.getRows().get(index).getCandidateId())) {
+                throw new AgentBridgeException(HttpStatus.BAD_GATEWAY, "ADAPTER_CONTRACT_INVALID",
+                        "閫傞厤鍣ㄨ繑鍥炵殑棰勬琛岄『搴忎笌璇锋眰涓嶄竴鑷�");
+            }
+        }
+    }
+
+    private void validateAttachment(Long attachmentId, String sourceSha256) {
+        String actual = attachmentService.sha256OfAttachment(attachmentId);
+        if (!StrUtil.equalsIgnoreCase(actual, sourceSha256)) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "SOURCE_CHANGED",
+                    "闄勪欢鍐呭宸插彉鍖栵紝璇烽噸鏂颁笂浼犲悗鍐嶈瘯", true, null);
+        }
+    }
+
+    private void validateRows(AgentImportPreflightReqVO request) {
+        if (request.getRows() == null || request.getRows().isEmpty()) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "ROWS_EMPTY", "娌℃湁鍙鍏ョ殑鏁版嵁琛�");
+        }
+        if (request.getRows().size() > maxRows) {
+            throw new AgentBridgeException(HttpStatus.UNPROCESSABLE_ENTITY, "ROWS_TOO_MANY",
+                    "鍗曟瀵煎叆瓒呰繃 " + maxRows + " 琛岋紝璇锋媶鍒嗘枃浠�");
+        }
+    }
+
+    private void requirePermission(AgentImportModuleAdapter adapter) {
+        for (String permission : adapter.permissions()) {
+            if (!securityFrameworkService.hasPermission(permission)) {
+                throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FORBIDDEN",
+                        "褰撳墠鐢ㄦ埛缂哄皯璇ユā鍧楃殑瀵煎叆鏉冮檺锛�" + permission + "锛�");
+            }
+        }
+    }
+
+    private AgentImportModuleAdapter requireAdapter(String moduleId) {
+        AgentImportModuleAdapter adapter = adapters().get(moduleId);
+        if (adapter == null) {
+            throw new AgentBridgeException(HttpStatus.NOT_FOUND, "MODULE_NOT_SUPPORTED",
+                    "涓嶆敮鎸佺殑瀵煎叆妯″潡锛�" + moduleId);
+        }
+        return adapter;
+    }
+
+    private Map<String, AgentImportModuleAdapter> adapters() {
+        Map<String, AgentImportModuleAdapter> result = new LinkedHashMap<>();
+        for (AgentImportModuleAdapter adapter : adapterProvider) {
+            result.put(adapter.moduleId(), adapter);
+        }
+        return result;
+    }
+
+    private static AgentImportCommandRowDO.AgentImportCommandRowDOBuilder baseRow(
+            String moduleId, String commandId, AgentImportPreflightReqVO.Row row) {
+        return AgentImportCommandRowDO.builder()
+                .commandId(commandId)
+                .moduleId(moduleId)
+                .candidateId(row.getCandidateId())
+                .sourceRow(row.getRowNo())
+                .code(row.getValues().get("code"))
+                .name(row.getValues().get("name"));
+    }
+
+    private static void requireSameDigest(AgentImportCommandDO command, String digest) {
+        if (!command.getRequestDigest().equals(digest)) {
+            throw new AgentBridgeException(HttpStatus.CONFLICT, "IDEMPOTENCY_CONFLICT",
+                    "鍚屼竴鍛戒护鎴栧箓绛夐敭鎼哄甫浜嗕笉鍚岀殑璇锋眰鍐呭锛岃妫�鏌ヤ换鍔$姸鎬佸悗閲嶈瘯");
+        }
+    }
+
+    private static String requestDigest(String moduleId, AgentImportCommitReqVO request) {
+        Map<String, Object> canonical = new LinkedHashMap<>();
+        canonical.put("module_id", moduleId);
+        canonical.put("attachment_id", request.getAttachmentId());
+        canonical.put("session_id", request.getSessionId());
+        canonical.put("source_sha256", StrUtil.nullToEmpty(request.getSourceSha256()).toLowerCase(Locale.ROOT));
+        canonical.put("preflight_hash", StrUtil.nullToEmpty(request.getPreflightHash()).toLowerCase(Locale.ROOT));
+        canonical.put("proposal_hash", StrUtil.nullToEmpty(request.getProposalHash()));
+        canonical.put("proposal_revision", StrUtil.nullToEmpty(request.getProposalRevision()));
+        canonical.put("options", request.getOptions() == null ? Map.of() : new TreeMap<>(request.getOptions()));
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("candidate_id", row.getCandidateId());
+            item.put("row_no", row.getRowNo());
+            item.put("values", row.getValues() == null ? Map.of() : new TreeMap<>(row.getValues()));
+            rows.add(item);
+        }
+        canonical.put("rows", rows);
+        try {
+            byte[] payload = OBJECT_MAPPER.writeValueAsBytes(canonical);
+            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload));
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("SHA-256 涓嶅彲鐢�", ex);
+        } catch (JsonProcessingException ex) {
+            throw new IllegalStateException("璇锋眰鎽樿鐢熸垚澶辫触", ex);
+        }
+    }
+
+    /**
+     * 閫傞厤鍣ㄨ绠楅妫�鎽樿鐨勭粺涓�鍏ュ彛锛岄伩鍏嶅悇妯″潡鎽樿鍙e緞婕傜Щ銆�
+     */
+    public static String preflightHash(String moduleId, AgentImportPreflightReqVO request,
+                                       Object resolvedPayload) {
+        Map<String, Object> canonical = new LinkedHashMap<>();
+        canonical.put("module_id", moduleId);
+        canonical.put("attachment_id", request.getAttachmentId());
+        canonical.put("source_sha256", StrUtil.nullToEmpty(request.getSourceSha256()).toLowerCase(Locale.ROOT));
+        canonical.put("proposal_revision", StrUtil.nullToEmpty(request.getProposalRevision()));
+        canonical.put("options", request.getOptions() == null ? Map.of() : new TreeMap<>(request.getOptions()));
+        List<Map<String, Object>> rows = new ArrayList<>();
+        for (AgentImportPreflightReqVO.Row row : request.getRows()) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("candidate_id", row.getCandidateId());
+            item.put("row_no", row.getRowNo());
+            item.put("values", row.getValues() == null ? Map.of() : new TreeMap<>(row.getValues()));
+            rows.add(item);
+        }
+        canonical.put("rows", rows);
+        canonical.put("resolved", resolvedPayload);
+        try {
+            byte[] payload = OBJECT_MAPPER.writeValueAsBytes(canonical);
+            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload));
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("SHA-256 涓嶅彲鐢�", ex);
+        } catch (JsonProcessingException ex) {
+            throw new IllegalStateException("棰勬鎽樿鐢熸垚澶辫触", ex);
+        }
+    }
+
+    private AgentImportReceiptRespVO toReceipt(AgentImportCommandDO command) {
+        String status = command.getStatus();
+        if (STATUS_RUNNING.equals(status) && command.getLeaseExpiresAt() != null
+                && command.getLeaseExpiresAt().isBefore(LocalDateTime.now())) {
+            status = STATUS_UNKNOWN;
+        }
+        List<AgentImportCommandRowDO> rowDOs = commandRowMapper.selectListByCommandId(command.getCommandId());
+        List<AgentImportReceiptRespVO.Row> rows = rowDOs.stream()
+                .map(row -> AgentImportReceiptRespVO.Row.builder()
+                        .candidateId(row.getCandidateId())
+                        .rowNo(row.getSourceRow())
+                        .code(row.getCode())
+                        .name(row.getName())
+                        .action(row.getAction())
+                        .status(row.getStatus())
+                        .targetItemId(row.getTargetItemId())
+                        .targetVersion(row.getTargetVersion())
+                        .errorCode(row.getErrorCode())
+                        .errorMessage(row.getErrorMessage())
+                        .retryable(row.getRetryable())
+                        .build())
+                .toList();
+        int success = (int) rowDOs.stream().filter(row -> "SUCCESS".equals(row.getStatus())).count();
+        int failed = (int) rowDOs.stream().filter(row -> "FAILED".equals(row.getStatus())).count();
+        int skipped = (int) rowDOs.stream().filter(row -> "SKIPPED".equals(row.getStatus())).count();
+        int unknown = (int) rowDOs.stream().filter(row -> "UNKNOWN".equals(row.getStatus())).count();
+        return AgentImportReceiptRespVO.builder()
+                .commandId(command.getCommandId())
+                .idempotencyKey(command.getIdempotencyKey())
+                .status(status)
+                .attachmentId(command.getAttachmentId())
+                .sessionId(command.getSessionId())
+                .sourceSha256(command.getSourceSha256())
+                .preflightHash(command.getPreflightHash())
+                .proposalHash(command.getProposalHash())
+                .summary(AgentImportReceiptRespVO.Summary.builder()
+                        .total(command.getTotalCount())
+                        .success(success).failed(failed).skipped(skipped).unknown(unknown)
+                        .build())
+                .errorCode(command.getErrorCode())
+                .errorMessage(command.getErrorMessage())
+                .rows(rows)
+                .createTime(command.getCreateTime())
+                .completedTime(STATUS_RUNNING.equals(command.getStatus()) ? null : command.getUpdateTime())
+                .build();
+    }
+
+    private static String trim(String value, int maxLength) {
+        if (value == null) {
+            return null;
+        }
+        return value.length() <= maxLength ? value : value.substring(0, maxLength);
+    }
+}

--
Gitblit v1.9.3