|
@@ -0,0 +1,437 @@
|
|
|
|
|
+package com.lingyue.graph.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import com.lingyue.document.entity.DocumentElement;
|
|
|
|
|
+import com.lingyue.document.repository.DocumentElementRepository;
|
|
|
|
|
+import com.lingyue.graph.dto.*;
|
|
|
|
|
+import com.lingyue.graph.entity.DataSource;
|
|
|
|
|
+import com.lingyue.graph.entity.GraphNode;
|
|
|
|
|
+import com.lingyue.graph.repository.DataSourceRepository;
|
|
|
|
|
+import com.lingyue.graph.repository.GraphNodeRepository;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 数据源服务
|
|
|
|
|
+ * 管理数据源的 CRUD 和值获取
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author lingyue
|
|
|
|
|
+ * @since 2026-01-21
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class DataSourceService {
|
|
|
|
|
+
|
|
|
|
|
+ private final DataSourceRepository dataSourceRepository;
|
|
|
|
|
+ private final GraphNodeRepository graphNodeRepository;
|
|
|
|
|
+ private final DocumentElementRepository documentElementRepository;
|
|
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== CRUD 操作 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建数据源
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public DataSource create(String userId, CreateDataSourceRequest request) {
|
|
|
|
|
+ DataSource dataSource = new DataSource();
|
|
|
|
|
+ dataSource.setUserId(userId);
|
|
|
|
|
+ dataSource.setName(request.getName());
|
|
|
|
|
+ dataSource.setType(request.getType());
|
|
|
|
|
+ dataSource.setDocumentId(request.getDocumentId());
|
|
|
|
|
+ dataSource.setSourceType(request.getSourceType() != null ? request.getSourceType() : "manual");
|
|
|
|
|
+ dataSource.setValueType(request.getValueType() != null ? request.getValueType() : "text");
|
|
|
|
|
+ dataSource.setAggregateType(request.getAggregateType() != null ? request.getAggregateType() : "first");
|
|
|
|
|
+ dataSource.setSeparator(request.getSeparator() != null ? request.getSeparator() : "");
|
|
|
|
|
+ dataSource.setNodeIds(request.getNodeIds());
|
|
|
|
|
+ dataSource.setConfig(request.getConfig());
|
|
|
|
|
+ dataSource.setMetadata(request.getMetadata());
|
|
|
|
|
+
|
|
|
|
|
+ dataSourceRepository.insert(dataSource);
|
|
|
|
|
+ log.info("创建数据源: id={}, name={}, type={}", dataSource.getId(), dataSource.getName(), dataSource.getType());
|
|
|
|
|
+ return dataSource;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取数据源
|
|
|
|
|
+ */
|
|
|
|
|
+ public DataSource getById(String id) {
|
|
|
|
|
+ return dataSourceRepository.selectById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按文档ID查询
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<DataSource> getByDocumentId(String documentId) {
|
|
|
|
|
+ return dataSourceRepository.findByDocumentId(documentId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按用户ID查询
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<DataSource> getByUserId(String userId) {
|
|
|
|
|
+ return dataSourceRepository.findByUserId(userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按类型查询
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<DataSource> getByUserIdAndType(String userId, String type) {
|
|
|
|
|
+ return dataSourceRepository.findByUserIdAndType(userId, type);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新数据源
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public DataSource update(String id, UpdateDataSourceRequest request) {
|
|
|
|
|
+ DataSource dataSource = dataSourceRepository.selectById(id);
|
|
|
|
|
+ if (dataSource == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("数据源不存在: " + id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (request.getName() != null) {
|
|
|
|
|
+ dataSource.setName(request.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getType() != null) {
|
|
|
|
|
+ dataSource.setType(request.getType());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getSourceType() != null) {
|
|
|
|
|
+ dataSource.setSourceType(request.getSourceType());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getValueType() != null) {
|
|
|
|
|
+ dataSource.setValueType(request.getValueType());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getAggregateType() != null) {
|
|
|
|
|
+ dataSource.setAggregateType(request.getAggregateType());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getSeparator() != null) {
|
|
|
|
|
+ dataSource.setSeparator(request.getSeparator());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getConfig() != null) {
|
|
|
|
|
+ dataSource.setConfig(request.getConfig());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getMetadata() != null) {
|
|
|
|
|
+ dataSource.setMetadata(request.getMetadata());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dataSourceRepository.updateById(dataSource);
|
|
|
|
|
+ log.info("更新数据源: id={}", id);
|
|
|
|
|
+ return dataSource;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新节点引用
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public DataSource updateRefs(String id, UpdateRefsRequest request) {
|
|
|
|
|
+ DataSource dataSource = dataSourceRepository.selectById(id);
|
|
|
|
|
+ if (dataSource == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("数据源不存在: " + id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ NodeRefs currentRefs = parseNodeRefs(dataSource.getNodeIds());
|
|
|
|
|
+ List<NodeRef> newRefs = request.getRefs() != null ? request.getRefs() : new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (request.isReplace()) {
|
|
|
|
|
+ // 替换模式
|
|
|
|
|
+ currentRefs.setRefs(newRefs);
|
|
|
|
|
+ } else if (request.isAppend()) {
|
|
|
|
|
+ // 追加模式
|
|
|
|
|
+ currentRefs.getRefs().addAll(newRefs);
|
|
|
|
|
+ } else if (request.isRemove()) {
|
|
|
|
|
+ // 移除模式
|
|
|
|
|
+ Set<String> idsToRemove = newRefs.stream()
|
|
|
|
|
+ .map(NodeRef::getId)
|
|
|
|
|
+ .collect(Collectors.toSet());
|
|
|
|
|
+ currentRefs.setRefs(
|
|
|
|
|
+ currentRefs.getRefs().stream()
|
|
|
|
|
+ .filter(ref -> !idsToRemove.contains(ref.getId()))
|
|
|
|
|
+ .collect(Collectors.toList())
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ dataSource.setNodeIds(currentRefs);
|
|
|
|
|
+ dataSourceRepository.updateById(dataSource);
|
|
|
|
|
+ log.info("更新数据源引用: id={}, mode={}, count={}", id, request.getMode(), currentRefs.size());
|
|
|
|
|
+ return dataSource;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除数据源
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional
|
|
|
|
|
+ public void delete(String id) {
|
|
|
|
|
+ dataSourceRepository.deleteById(id);
|
|
|
|
|
+ log.info("删除数据源: id={}", id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 核心取值逻辑 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取数据源的值
|
|
|
|
|
+ * 核心方法:从绑定的节点中提取并聚合值
|
|
|
|
|
+ */
|
|
|
|
|
+ public DataSourceValue getValue(String dataSourceId) {
|
|
|
|
|
+ DataSource dataSource = dataSourceRepository.selectById(dataSourceId);
|
|
|
|
|
+ if (dataSource == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("数据源不存在: " + dataSourceId);
|
|
|
|
|
+ }
|
|
|
|
|
+ return getValue(dataSource);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取数据源的值
|
|
|
|
|
+ */
|
|
|
|
|
+ public DataSourceValue getValue(DataSource dataSource) {
|
|
|
|
|
+ NodeRefs refs = parseNodeRefs(dataSource.getNodeIds());
|
|
|
|
|
+
|
|
|
|
|
+ if (refs.isEmpty()) {
|
|
|
|
|
+ return DataSourceValue.empty(dataSource.getId(), dataSource.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取所有节点值
|
|
|
|
|
+ List<NodeValueItem> items = fetchNodeValues(refs);
|
|
|
|
|
+
|
|
|
|
|
+ if (items.isEmpty()) {
|
|
|
|
|
+ return DataSourceValue.empty(dataSource.getId(), dataSource.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 根据值类型和聚合方式生成返回值
|
|
|
|
|
+ return aggregateValues(dataSource, items);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 批量获取数据源值
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, DataSourceValue> batchGetValue(List<String> dataSourceIds) {
|
|
|
|
|
+ Map<String, DataSourceValue> result = new LinkedHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (dataSourceIds == null || dataSourceIds.isEmpty()) {
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 批量查询数据源
|
|
|
|
|
+ List<DataSource> dataSources = dataSourceRepository.selectBatchIds(dataSourceIds);
|
|
|
|
|
+
|
|
|
|
|
+ for (DataSource dataSource : dataSources) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ DataSourceValue value = getValue(dataSource);
|
|
|
|
|
+ result.put(dataSource.getId(), value);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("获取数据源值失败: id={}, error={}", dataSource.getId(), e.getMessage());
|
|
|
|
|
+ result.put(dataSource.getId(), DataSourceValue.empty(dataSource.getId(), dataSource.getName()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 私有方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析节点引用
|
|
|
|
|
+ */
|
|
|
|
|
+ private NodeRefs parseNodeRefs(Object nodeIds) {
|
|
|
|
|
+ if (nodeIds == null) {
|
|
|
|
|
+ return new NodeRefs();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (nodeIds instanceof NodeRefs) {
|
|
|
|
|
+ return (NodeRefs) nodeIds;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (nodeIds instanceof Map) {
|
|
|
|
|
+ return objectMapper.convertValue(nodeIds, NodeRefs.class);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (nodeIds instanceof String) {
|
|
|
|
|
+ return objectMapper.readValue((String) nodeIds, NodeRefs.class);
|
|
|
|
|
+ }
|
|
|
|
|
+ return objectMapper.convertValue(nodeIds, NodeRefs.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析节点引用失败: {}", e.getMessage());
|
|
|
|
|
+ return new NodeRefs();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取所有节点的值
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<NodeValueItem> fetchNodeValues(NodeRefs refs) {
|
|
|
|
|
+ List<NodeValueItem> items = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取图节点
|
|
|
|
|
+ List<String> graphNodeIds = refs.getGraphNodeIds();
|
|
|
|
|
+ if (!graphNodeIds.isEmpty()) {
|
|
|
|
|
+ List<GraphNode> graphNodes = graphNodeRepository.selectBatchIds(graphNodeIds);
|
|
|
|
|
+ for (GraphNode node : graphNodes) {
|
|
|
|
|
+ items.add(createValueItemFromGraphNode(node));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取文档元素
|
|
|
|
|
+ List<String> elementIds = refs.getDocumentElementIds();
|
|
|
|
|
+ if (!elementIds.isEmpty()) {
|
|
|
|
|
+ List<DocumentElement> elements = documentElementRepository.selectBatchIds(elementIds);
|
|
|
|
|
+ for (DocumentElement element : elements) {
|
|
|
|
|
+ items.add(createValueItemFromElement(element));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return items;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从图节点创建值项
|
|
|
|
|
+ */
|
|
|
|
|
+ private NodeValueItem createValueItemFromGraphNode(GraphNode node) {
|
|
|
|
|
+ return NodeValueItem.text(
|
|
|
|
|
+ NodeRef.TYPE_GRAPH_NODE,
|
|
|
|
|
+ node.getId(),
|
|
|
|
|
+ node.getValue(),
|
|
|
|
|
+ node.getName()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从文档元素创建值项
|
|
|
|
|
+ */
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private NodeValueItem createValueItemFromElement(DocumentElement element) {
|
|
|
|
|
+ String elementType = element.getElementType();
|
|
|
|
|
+
|
|
|
|
|
+ if ("image".equals(elementType)) {
|
|
|
|
|
+ return NodeValueItem.image(
|
|
|
|
|
+ NodeRef.TYPE_DOCUMENT_ELEMENT,
|
|
|
|
|
+ element.getId(),
|
|
|
|
|
+ element.getImageUrl(),
|
|
|
|
|
+ element.getImagePath(),
|
|
|
|
|
+ element.getImageAlt()
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if ("table".equals(elementType)) {
|
|
|
|
|
+ return NodeValueItem.table(
|
|
|
|
|
+ NodeRef.TYPE_DOCUMENT_ELEMENT,
|
|
|
|
|
+ element.getId(),
|
|
|
|
|
+ element.getTableData(),
|
|
|
|
|
+ "表格 #" + element.getTableIndex()
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 文本类型(paragraph, heading 等)
|
|
|
|
|
+ return NodeValueItem.text(
|
|
|
|
|
+ NodeRef.TYPE_DOCUMENT_ELEMENT,
|
|
|
|
|
+ element.getId(),
|
|
|
|
|
+ element.getContent(),
|
|
|
|
|
+ elementType
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 聚合值
|
|
|
|
|
+ */
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ private DataSourceValue aggregateValues(DataSource dataSource, List<NodeValueItem> items) {
|
|
|
|
|
+ String valueType = dataSource.getValueType();
|
|
|
|
|
+ String aggregateType = dataSource.getAggregateType();
|
|
|
|
|
+ String separator = dataSource.getSeparator() != null ? dataSource.getSeparator() : "";
|
|
|
|
|
+
|
|
|
|
|
+ // 图片类型
|
|
|
|
|
+ if ("image".equals(valueType)) {
|
|
|
|
|
+ NodeValueItem firstImage = items.stream()
|
|
|
|
|
+ .filter(item -> "image".equals(item.getValueType()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (firstImage != null) {
|
|
|
|
|
+ return DataSourceValue.image(
|
|
|
|
|
+ dataSource.getId(),
|
|
|
|
|
+ dataSource.getName(),
|
|
|
|
|
+ firstImage.getImageUrl(),
|
|
|
|
|
+ firstImage.getImagePath(),
|
|
|
|
|
+ items
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ return DataSourceValue.empty(dataSource.getId(), dataSource.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 表格类型
|
|
|
|
|
+ if ("table".equals(valueType)) {
|
|
|
|
|
+ NodeValueItem firstTable = items.stream()
|
|
|
|
|
+ .filter(item -> "table".equals(item.getValueType()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ if (firstTable != null) {
|
|
|
|
|
+ return DataSourceValue.table(
|
|
|
|
|
+ dataSource.getId(),
|
|
|
|
|
+ dataSource.getName(),
|
|
|
|
|
+ firstTable.getTableData(),
|
|
|
|
|
+ items
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ return DataSourceValue.empty(dataSource.getId(), dataSource.getName());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 文本类型 - 需要聚合
|
|
|
|
|
+ List<String> textValues = items.stream()
|
|
|
|
|
+ .filter(item -> "text".equals(item.getValueType()) && item.getTextValue() != null)
|
|
|
|
|
+ .map(NodeValueItem::getTextValue)
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+
|
|
|
|
|
+ String aggregatedText;
|
|
|
|
|
+ switch (aggregateType) {
|
|
|
|
|
+ case "first":
|
|
|
|
|
+ aggregatedText = textValues.isEmpty() ? "" : textValues.get(0);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "last":
|
|
|
|
|
+ aggregatedText = textValues.isEmpty() ? "" : textValues.get(textValues.size() - 1);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "concat":
|
|
|
|
|
+ aggregatedText = String.join(separator, textValues);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "sum":
|
|
|
|
|
+ aggregatedText = String.valueOf(textValues.stream()
|
|
|
|
|
+ .mapToDouble(this::parseDouble)
|
|
|
|
|
+ .sum());
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "avg":
|
|
|
|
|
+ aggregatedText = String.valueOf(textValues.stream()
|
|
|
|
|
+ .mapToDouble(this::parseDouble)
|
|
|
|
|
+ .average()
|
|
|
|
|
+ .orElse(0.0));
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "list":
|
|
|
|
|
+ default:
|
|
|
|
|
+ // list 模式返回所有项
|
|
|
|
|
+ return DataSourceValue.builder()
|
|
|
|
|
+ .dataSourceId(dataSource.getId())
|
|
|
|
|
+ .dataSourceName(dataSource.getName())
|
|
|
|
|
+ .type(DataSourceValue.TYPE_TEXT)
|
|
|
|
|
+ .textValue(String.join(separator.isEmpty() ? "\n" : separator, textValues))
|
|
|
|
|
+ .items(items)
|
|
|
|
|
+ .nodeCount(items.size())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return DataSourceValue.text(dataSource.getId(), dataSource.getName(), aggregatedText, items);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 安全解析数字
|
|
|
|
|
+ */
|
|
|
|
|
+ private double parseDouble(String value) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Double.parseDouble(value.replaceAll("[^0-9.\\-]", ""));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|