全新模型分析方法整体调整

topo_dev
DESKTOP-DDTUS3E\yaxin 6 months ago
parent e7921a26a4
commit 42ba581ea6

@ -10,45 +10,54 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/model")
@RequiredArgsConstructor
public class ModelController {
private static final String METHOD_NEW = "new";
private final ModelService modelService;
@Value("${fu-hsi-config.case-analysis-method}")
private String caseAnalysisMethod;
@PostMapping("/analyseCase")
@Operation(summary = "分析指标")
public R<?> analyseCase(@RequestBody AnalyseCaseDTO analyseCaseDTO) {
return modelService.analyseCase(analyseCaseDTO);
if (METHOD_NEW.equals(caseAnalysisMethod)) {
return modelService.analyseCaseNew(analyseCaseDTO);
} else {
return modelService.analyseCase(analyseCaseDTO);
}
}
@GetMapping("/caseScoreDetail")
@Operation(summary = "案件得分详情功能")
public R<CaseScoreDetailDTO> caseScoreDetail( @RequestParam @Parameter(name = "caseId",description = "案件id") String caseId) {
public R<CaseScoreDetailDTO> caseScoreDetail(@RequestParam @Parameter(name = "caseId", description = "案件id") String caseId) {
CaseScoreDetailDTO detail = modelService.caseScoreDetail(caseId);
return R.ok(detail);
}
@GetMapping("/caseScoreByCaseId")
@Operation(summary = "获取案件得分详情")
public R<CaseScore> caseScoreByCaseId(@RequestParam @Parameter(name = "caseId",description = "案件id") String caseId) {
public R<CaseScore> caseScoreByCaseId(@RequestParam @Parameter(name = "caseId", description = "案件id") String caseId) {
return R.ok(modelService.caseScoreByCaseId(caseId));
}
@GetMapping("/exportCaseScoreDetail")
@Operation(summary = "导出案件得分详情功能")
public void exportCaseScoreDetail( @RequestParam @Parameter(name = "caseId",description = "案件id") String caseId,
HttpServletResponse response) {
public void exportCaseScoreDetail(@RequestParam @Parameter(name = "caseId", description = "案件id") String caseId,
HttpServletResponse response) {
modelService.exportCaseScoreDetail(caseId,response);
modelService.exportCaseScoreDetail(caseId, response);
}
@GetMapping("/getCaseDateStatus")
@Operation(summary = "获取案件数据状态")
public R<CaseStatus> getCaseDateStatus(@RequestParam @Parameter(name = "caseId",description = "案件id") String caseId) {
public R<CaseStatus> getCaseDateStatus(@RequestParam @Parameter(name = "caseId", description = "案件id") String caseId) {
CaseStatus caseStatus = modelService.getCaseDateStatus(caseId);
return R.ok(caseStatus);

@ -1,15 +1,16 @@
package com.supervision.police.domain;
import java.time.LocalDateTime;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.supervision.police.dto.indexRule.IndexRule;
import com.supervision.police.handler.IndexRuleTypeHandler;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
@ -19,39 +20,39 @@ import java.util.List;
* @since 2024-07-05 09:20:10
*/
@Data
@TableName("model_index")
@TableName(value = "model_index", autoResultMap = true)
public class ModelIndex implements Serializable {
@TableId
private String id;
/**
*
*/
private String name;
/**
*
*/
private String shortName;
/**
*
*/
private String remark;
/**
*
*/
private String indexType;
@TableField(exist = false)
private String indexTypeName;
/**
*
*/
private Integer indexScore;
/**
*
*/
@ -64,7 +65,7 @@ public class ModelIndex implements Serializable {
private String caseType;
@TableField(exist = false)
private String caseTypeName;
/**
*
*/
@ -73,18 +74,19 @@ public class ModelIndex implements Serializable {
/**
*
*/
private String indexRule;
@TableField(typeHandler = IndexRuleTypeHandler.class)
private IndexRule indexRule;
@TableField(exist = false)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateStartTime;
@TableField(exist = false)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateEndTime;
/**
*
*/
@ -100,7 +102,7 @@ public class ModelIndex implements Serializable {
*
*/
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
/**
@ -112,7 +114,7 @@ public class ModelIndex implements Serializable {
*
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}

@ -14,7 +14,7 @@ public class IndexRule {
*
*/
private String logic = "2";
private String groupLogic = "2";
/**
*

@ -43,4 +43,11 @@ public class Operand{
* 0 1 2
*/
private String aggregateType;
/**
*
* 3
* 4
*/
private String relationalSymbol;
}

@ -10,7 +10,7 @@ public class RuleConditionGroup {
/**
*
*/
private String logic = "2";
private String rowLogic = "2";
/**
*

@ -0,0 +1,29 @@
package com.supervision.police.handler;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.supervision.police.dto.indexRule.IndexRule;
public class IndexRuleTypeHandler extends AbstractJsonTypeHandler<IndexRule> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
protected IndexRule parse(String json) {
try {
return objectMapper.readValue(json, new TypeReference<IndexRule>() {
});
} catch (Exception e) {
throw new RuntimeException("Failed to parse JSON to List<IndexRule>", e);
}
}
@Override
protected String toJson(IndexRule obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException("Failed to convert List<IndexRule> to JSON", e);
}
}
}

@ -2,9 +2,6 @@ package com.supervision.police.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.supervision.police.domain.ModelIndex;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* (ModelIndex)访
@ -13,8 +10,5 @@ import java.util.List;
* @since 2024-07-05 09:20:10
*/
public interface ModelIndexMapper extends BaseMapper<ModelIndex> {
List<ModelIndex> selectByCaseType(@Param("caseType") String caseType);
}

@ -8,7 +8,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.deepoove.poi.XWPFTemplate;
import com.google.gson.Gson;
import com.supervision.common.constant.IndexRuleConstants;
import com.supervision.common.domain.R;
import com.supervision.common.utils.StringUtils;
import com.supervision.constant.JudgeResultEnum;
@ -187,7 +187,7 @@ public class ModelServiceImpl implements ModelService {
throw new RuntimeException("未找到的行为人" + analyseCaseDTO.getLawActorName());
}
// 查出指标信息
List<ModelIndex> modelIndices = modelIndexMapper.selectByCaseType(modelCase.getCaseType());
List<ModelIndex> modelIndices = modelIndexService.list(new LambdaQueryWrapper<ModelIndex>().eq(ModelIndex::getDataStatus, "1").eq(ModelIndex::getCaseType, modelCase.getCaseType()));
// 查出原子指标信息
List<ModelAtomicIndex> atomicIndices = modelAtomicIndexMapper.selectByCaseType(modelCase.getCaseType());
// 查出提示词信息
@ -198,67 +198,86 @@ public class ModelServiceImpl implements ModelService {
List<CaseEvidence> caseEvidences = caseEvidenceService.list(new LambdaQueryWrapper<CaseEvidence>().eq(CaseEvidence::getCaseId, caseId));
// 遍历指标集合,处理每个指标的判断逻辑,得出结果
modelIndices.forEach(modelIndex -> {
String indexRuleStr = modelIndex.getIndexRule();
Gson gson = new Gson();
IndexRule indexRule = gson.fromJson(indexRuleStr, IndexRule.class);
IndexRule indexRule = modelIndex.getIndexRule();
Set<Boolean> ruleConditionGroupResultList = new HashSet<>();
indexRule.getRuleConditionGroupList().forEach(ruleConditionGroup -> {
Set<Boolean> ruleConditionResultSet = new HashSet<>();
ruleConditionGroup.getRuleConditionList().forEach(ruleCondition -> {
Set<Boolean> operandUnitResultSet = new HashSet<>();
ruleCondition.getOperandUnitList().forEach(operandUnit -> {
Operand left = operandUnit.getLeftOperand();
Operand right = operandUnit.getRightOperand();
ModelAtomicIndex modelAtomicIndex = atomicIndices.stream().filter(atomicIndex -> atomicIndex.getId().equals(left.getAtomicIndexId())).findAny().orElse(null);
if (modelAtomicIndex == null) {
log.error("原子指标不存在,跳出当前循环。原子指标ID:{}", left.getAtomicIndexId());
return;
}
// 定义原子指标结果共有属性
ModelAtomicResult result = new ModelAtomicResult();
result.setIndexId(modelIndex.getId());
result.setCasePersonId(casePerson.getId());
result.setCaseId(analyseCaseDTO.getCaseId());
result.setAtomicId(modelAtomicIndex.getId());
result.setAtomicResult(JudgeResultEnum.NOT_EXIST.getCode());
ModelAtomicResult exist = modelAtomicResultMapper.selectByCaseIdAndAtomicId(caseId, casePerson.getId(), modelIndex.getId(), modelAtomicIndex.getId());
if (exist != null) {
result.setId(exist.getId());
}
switch (left.getOperandType()) {
case OPERAND_TYPE_MANUAL:
operandUnitResultSet.add(manualIndexAnalysis(left, caseId));
break;
case OPERAND_TYPE_DB:
operandUnitResultSet.add(dbIndexAnalysis(caseId, modelAtomicIndex.getQueryLang(), result));
break;
case OPERAND_TYPE_GRAPH:
operandUnitResultSet.add(graphIndexAnalysis(modelIndex, casePerson, modelAtomicIndex, analyseCaseDTO, result));
break;
case OPERAND_TYPE_STRUCTURE:
operandUnitResultSet.add(structureIndexAnalysis(left, right, operandUnit.getOperator(), modelAtomicIndex, atomicIndices, notePrompts, evidenceDirectories, caseEvidences, result));
break;
default:
break;
}
if (indexRule != null) {
indexRule.getRuleConditionGroupList().forEach(ruleConditionGroup -> {
Set<Boolean> ruleConditionResultSet = new HashSet<>();
ruleConditionGroup.getRuleConditionList().forEach(ruleCondition -> {
Set<Boolean> operandUnitResultSet = new HashSet<>();
ruleCondition.getOperandUnitList().forEach(operandUnit -> {
Operand left = operandUnit.getLeftOperand();
boolean relationSymbol = IndexRuleConstants.EVALUATE_RESULT_EXIST.equals(left.getRelationalSymbol());
Operand right = operandUnit.getRightOperand();
ModelAtomicIndex modelAtomicIndex = atomicIndices.stream().filter(atomicIndex -> atomicIndex.getId().equals(left.getAtomicIndexId())).findAny().orElse(null);
if (modelAtomicIndex == null) {
log.error("原子指标不存在,跳出当前循环。原子指标ID:{}", left.getAtomicIndexId());
return;
}
// 定义原子指标结果共有属性
ModelAtomicResult result = new ModelAtomicResult();
result.setIndexId(modelIndex.getId());
result.setCasePersonId(casePerson.getId());
result.setCaseId(analyseCaseDTO.getCaseId());
result.setAtomicId(modelAtomicIndex.getId());
result.setAtomicResult(JudgeResultEnum.NOT_EXIST.getCode());
ModelAtomicResult exist = modelAtomicResultMapper.selectByCaseIdAndAtomicId(caseId, casePerson.getId(), modelIndex.getId(), modelAtomicIndex.getId());
if (exist != null) {
result.setId(exist.getId());
}
switch (left.getOperandType()) {
case OPERAND_TYPE_MANUAL:
operandUnitResultSet.add(relationSymbol == manualIndexAnalysis(left.getAtomicIndexId(), caseId));
break;
case OPERAND_TYPE_DB:
operandUnitResultSet.add(relationSymbol == dbIndexAnalysis(caseId, modelAtomicIndex.getQueryLang(), result));
break;
case OPERAND_TYPE_GRAPH:
operandUnitResultSet.add(relationSymbol == graphIndexAnalysis(casePerson.getName(), modelAtomicIndex, analyseCaseDTO, result));
break;
case OPERAND_TYPE_STRUCTURE:
operandUnitResultSet.add(structureIndexAnalysis(left, right, operandUnit.getOperator(), modelAtomicIndex, atomicIndices, notePrompts, evidenceDirectories, caseEvidences, result));
break;
default:
break;
}
});
ruleConditionResultSet.add(CalculationUtil.calculateBooleanSet(operandUnitResultSet, ruleCondition.getLogic()));
});
ruleConditionResultSet.add(CalculationUtil.calculateBooleanSet(operandUnitResultSet, ruleCondition.getLogic()));
ruleConditionGroupResultList.add(CalculationUtil.calculateBooleanSet(ruleConditionResultSet, ruleConditionGroup.getRowLogic()));
});
ruleConditionGroupResultList.add(CalculationUtil.calculateBooleanSet(ruleConditionResultSet, ruleConditionGroup.getLogic()));
});
boolean result = CalculationUtil.calculateBooleanSet(ruleConditionGroupResultList, indexRule.getLogic());
} else {
log.error("指标规则不存在,跳出当前循环。指标ID:{}", modelIndex.getId());
return;
}
// 计算指标结果并保存
boolean result = CalculationUtil.calculateBooleanSet(ruleConditionGroupResultList, indexRule.getGroupLogic());
ModelIndexResult modelIndexResult = new ModelIndexResult();
modelIndexResult.setCaseId(caseId);
modelIndexResult.setIndexId(modelIndex.getId());
ModelIndexResult exist = modelIndexResultMapper.selectByCaseIdAndIndexId(analyseCaseDTO.getCaseId(), modelIndex.getId());
if (exist != null) {
modelIndexResult.setId(exist.getId());
}
modelIndexResult.setIndexResult(result ? "true" : "false");
if (exist == null) {
modelIndexResultMapper.insert(modelIndexResult);
} else {
modelIndexResultMapper.updateById(modelIndexResult);
}
if (result) {
Integer score = typeScoreMap.getOrDefault(modelIndex.getIndexType(), 0);
log.info("指标ID:{},指标类型:{},得分:{}分", modelIndex.getId(), modelIndex.getIndexType(), modelIndex.getIndexScore());
typeScoreMap.put(modelIndex.getIndexType(), score + modelIndex.getIndexScore());
log.info("当前类型总分:{}分", typeScoreMap.get(modelIndex.getIndexType()));
}
});
log.info("计算分数(共性+入罪/共性+出罪 取最大值)");
Integer gx = typeScoreMap.getOrDefault("1", 0);
Integer rz = typeScoreMap.getOrDefault("2", 0);
Integer cz = typeScoreMap.getOrDefault("3", 0);
int max = Integer.max(gx + rz, gx + cz);
modelCase.setTotalScore(max);
log.info("更新案件得分情况");
log.info("更新案件得分情况。最终得分:{}分(共性+入罪/共性+出罪 取最大值)。入罪:{}分。出罪:{}分。共性:{}分。", max, rz, cz, gx);
caseStatusManageService.whenAnalyseCaseSuccess(analyseCaseDTO.getCaseId(), modelCase.getTotalScore());
noteRecordService.uploadFileToLangChainChat(analyseCaseDTO.getCaseId());
return R.ok();
@ -267,14 +286,14 @@ public class ModelServiceImpl implements ModelService {
/**
*
*
* @param left
* @param caseId ID
* @param atomicIndexId ID
* @param caseId ID
*/
private boolean manualIndexAnalysis(Operand left, String caseId) {
private boolean manualIndexAnalysis(String atomicIndexId, String caseId) {
boolean flag = false;
List<ModelAtomicResult> modelAtomicResults = modelAtomicResultMapper.selectList(
new LambdaQueryWrapper<ModelAtomicResult>().eq(ModelAtomicResult::getCaseId, caseId)
.eq(ModelAtomicResult::getAtomicId, left.getAtomicIndexId()));
.eq(ModelAtomicResult::getAtomicId, atomicIndexId));
if (modelAtomicResults != null && !modelAtomicResults.isEmpty()) {
ModelAtomicResult modelAtomicResult = CollUtil.getFirst(modelAtomicResults);
flag = EVALUATE_RESULT_EXIST.equals(modelAtomicResult.getAtomicResult());
@ -308,19 +327,18 @@ public class ModelServiceImpl implements ModelService {
/**
*
*
* @param modelIndex
* @param casePerson
* @param casePersonName
* @param modelAtomicIndex
* @param analyseCaseDTO
* @return
*/
private boolean graphIndexAnalysis(ModelIndex modelIndex, CasePerson casePerson, ModelAtomicIndex modelAtomicIndex, AnalyseCaseDTO analyseCaseDTO, ModelAtomicResult atomicResult) {
private boolean graphIndexAnalysis(String casePersonName, ModelAtomicIndex modelAtomicIndex, AnalyseCaseDTO analyseCaseDTO, ModelAtomicResult atomicResult) {
boolean flag = false;
Session session = driver.session();
//图谱
Map<String, Object> params = new HashMap<>();
// 行为人
params.put("lawActor", casePerson.getName());
params.put("lawActor", casePersonName);
// 案号
params.put("caseId", analyseCaseDTO.getCaseId());
Result run = null;
@ -599,10 +617,8 @@ public class ModelServiceImpl implements ModelService {
*
*/
private void calculateFinalScore(AnalyseCaseDTO analyseCaseDTO, ModelCase modelCase, Map<String, Map<String, String>> atomicResultMap) {
// 计算指标结果
int score = 0;
// 根据案件类型获取所有的指标
List<ModelIndex> modelIndices = modelIndexMapper.selectByCaseType(modelCase.getCaseType());
List<ModelIndex> modelIndices = modelIndexService.list(new LambdaQueryWrapper<ModelIndex>().eq(ModelIndex::getDataStatus, "1").eq(ModelIndex::getCaseType, modelCase.getCaseType()));
Map<String, Integer> typeScoreMap = new HashMap<>();
for (ModelIndex modelIndex : modelIndices) {
ModelIndexResult result = new ModelIndexResult();

@ -41,17 +41,17 @@ public class CalculationUtil {
public static boolean calculateBooleanSet(Set<Boolean> booleanSet, String logic) {
// 判断是否为空
if (booleanSet == null || booleanSet.isEmpty()) {
throw new IllegalArgumentException("Boolean set cannot be null or empty");
return false;
}
// 判断是"且"还是"或"
String operator;
String operator = "";
if (LOGIC_AND.equals(logic)) {
operator = " and ";
} else if (LOGIC_OR.equals(logic)) {
operator = " or ";
} else {
throw new IllegalArgumentException("Invalid logic value, use 1 for AND, 2 for OR.");
throw new IllegalArgumentException("Invalid logic value: [" + operator + "], use 1 for AND, 2 for OR.");
}
// 构建表达式

@ -26,6 +26,7 @@ case:
evidence:
table: case_evidence
fu-hsi-config:
case-analysis-method: new
thread-pool:
triple:
core: 1

@ -1,7 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.supervision.police.mapper.ModelIndexMapper">
<select id="selectByCaseType" resultType="com.supervision.police.domain.ModelIndex">
select * from model_index where data_status = '1' and case_type = #{caseType}
</select>
</mapper>

@ -0,0 +1,80 @@
package com.supervision.demo;
import cn.hutool.json.JSONUtil;
import com.supervision.police.domain.ModelAtomicIndex;
import com.supervision.police.domain.ModelIndex;
import com.supervision.police.dto.AnalyseCaseDTO;
import com.supervision.police.dto.JudgeLogic;
import com.supervision.police.dto.indexRule.*;
import com.supervision.police.service.ModelAtomicIndexService;
import com.supervision.police.service.ModelIndexService;
import com.supervision.police.service.ModelService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@SpringBootTest
public class CaseTest {
@Autowired
private ModelService modelService;
@Autowired
private ModelIndexService modelIndexService;
@Autowired
private ModelAtomicIndexService modelAtomicIndexService;
@Test
public void test() {
long start = System.currentTimeMillis();
AnalyseCaseDTO analyseCaseDTO = new AnalyseCaseDTO();
analyseCaseDTO.setCaseId("1831221360763416578");
modelService.analyseCaseNew(analyseCaseDTO);
long end = System.currentTimeMillis();
log.info("耗时:{}ms", end - start);
}
@Test
public void judgeLogicTransform() {
List<ModelIndex> indices = modelIndexService.list();
List<ModelAtomicIndex> modelAtomicIndices = modelAtomicIndexService.list();
indices.forEach(index -> {
String judgeLogicStr = index.getJudgeLogic();
List<JudgeLogic> judgeLogics = JSONUtil.toList(judgeLogicStr, JudgeLogic.class);
IndexRule indexRule = new IndexRule();
if (judgeLogics.size() == 2) {
indexRule.setGroupLogic(judgeLogics.get(1).getGroupLogic());
}
List<RuleConditionGroup> ruleConditionGroups = new ArrayList<>();
judgeLogics.forEach(judgeLogic -> {
RuleConditionGroup ruleConditionGroup = new RuleConditionGroup();
List<RuleCondition> ruleConditions = new ArrayList<>();
if (!judgeLogic.getRowLogic().equals("&")) {
ruleConditionGroup.setRowLogic(judgeLogic.getRowLogic());
}
judgeLogic.getAtomicData().forEach(atomicData -> {
RuleCondition ruleCondition = new RuleCondition();
OperandUnit operandUnit = new OperandUnit();
Operand left = new Operand();
left.setAtomicIndexId(atomicData.getAtomicIndex());
left.setRelationalSymbol(atomicData.getRelationalSymbol());
modelAtomicIndices.stream().filter(modelAtomicIndex -> modelAtomicIndex.getId().equals(atomicData.getAtomicIndex())).findFirst().ifPresent(modelAtomicIndex -> {
left.setOperandType(modelAtomicIndex.getIndexSource());
});
operandUnit.setLeftOperand(left);
ruleCondition.setOperandUnitList(List.of(operandUnit));
ruleConditions.add(ruleCondition);
});
ruleConditionGroup.setRuleConditionList(ruleConditions);
ruleConditionGroups.add(ruleConditionGroup);
});
indexRule.setRuleConditionGroupList(ruleConditionGroups);
index.setIndexRule(indexRule);
modelIndexService.updateById(index);
});
}
}
Loading…
Cancel
Save