代码提交
This commit is contained in:
1 parent
4d54cc42e0
commit
13990c058a
100 files changed
+13184
No files matched your search
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package net.hzvtc.es.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.utils.file.FileUtils;
|
||||||
|
import net.hzvtc.service.EsRecordReportService;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/es/result/report")
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EsRecordReportController {
|
||||||
|
private final EsRecordReportService esRecordReportService;
|
||||||
|
|
||||||
|
@PostMapping("/getBirdStatData")
|
||||||
|
public AjaxResult getBirdStatData(@RequestBody JSONObject object) {
|
||||||
|
return AjaxResult.success(esRecordReportService.getBirdDailyReportStatByParam(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/downloadDetailsReport")
|
||||||
|
public void downloadReport(@RequestParam Map<String, Object> param, HttpServletResponse response, HttpServletRequest request) {
|
||||||
|
|
||||||
|
// obj.put("dateFrom", param.get("dateFrom"));
|
||||||
|
// obj.put("dateTo", param.get("dateTo"));
|
||||||
|
// obj.put("excludeIds", param.get("excludeIds"));
|
||||||
|
// log.info("=============={}", obj);
|
||||||
|
String filePath = esRecordReportService.exportResultRecordListFileByParam(param);
|
||||||
|
// log.info("===========fileName============{}", filePath);
|
||||||
|
if (filePath != null) {
|
||||||
|
try {
|
||||||
|
String realFileName = System.currentTimeMillis() + ".xlsx";
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
FileUtils.setAttachmentResponseHeader(response, realFileName);
|
||||||
|
FileUtils.writeBytes(filePath, response.getOutputStream());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("error", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// @PostMapping("/sendEmail")
|
||||||
|
// public AjaxResult sendEmail(@RequestBody JSONObject object) {
|
||||||
|
// return AjaxResult.success(esRecordReportService.sendBirdDataStatEmail(object));
|
||||||
|
// }
|
||||||
|
@PostMapping("/speciesMediaStatList")
|
||||||
|
public JSONObject getSpeciesMediaListByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getSpeciesMediaListByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/recordStatList")
|
||||||
|
public JSONObject getRecordStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getResultRecordStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/areaStatList")
|
||||||
|
public JSONObject getAreaStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getAreaStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dateStatList")
|
||||||
|
public JSONObject getRecordStatByDate(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getRecordStatDateReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dateValidStatList")
|
||||||
|
public JSONObject getValidStatByDate(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getValidStatDateReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/recordValidStatList")
|
||||||
|
public JSONObject getRecordValidStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getResultRecordValidStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/heatStatList")
|
||||||
|
public JSONObject getRecordValidHeataStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getHeatValidStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/recordDailyStatList")
|
||||||
|
public JSONObject getRecordDailyStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getRecordDailyStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cameraStatList")
|
||||||
|
public JSONObject getCameraStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getCameraRecordStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/habitatStatList")
|
||||||
|
public JSONObject getHabitatStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getSpeciesHabitatStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/raiStatList")
|
||||||
|
public JSONObject getRAIStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getValidRAIReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/rnStatList")
|
||||||
|
public JSONObject getRNStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getValidRNReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/percentStatList")
|
||||||
|
public JSONObject getPercentStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getValidPercentReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/rrpStatList")
|
||||||
|
public JSONObject getSpeciesRRPStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getValidRAIRNPtReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/rhythmStatList")
|
||||||
|
public JSONObject getRhythmStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getRhythmReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/multiStatList")
|
||||||
|
public JSONObject getMultiStatByParam(@RequestBody JSONObject object) {
|
||||||
|
return esRecordReportService.getSpeciesMultiStatReportByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/speciesList")
|
||||||
|
public JSONObject getSpeciesList() {
|
||||||
|
return esRecordReportService.getSpeciesTreeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/periodStat")
|
||||||
|
public AjaxResult getBirdPeriodStat(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("type", 1);
|
||||||
|
return AjaxResult.success(esRecordReportService.getBirdPeriodStatByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/monthStat")
|
||||||
|
public AjaxResult getMonthStat(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("type", 2);
|
||||||
|
return AjaxResult.success(esRecordReportService.getBirdPeriodStatByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/yearStat")
|
||||||
|
public AjaxResult getYearStat(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("type", 3);
|
||||||
|
return AjaxResult.success(esRecordReportService.getBirdPeriodStatByParam(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
package net.hzvtc.es.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.core.page.TableDataInfo;
|
||||||
|
import net.hzvtc.device.domain.DeviceInfo;
|
||||||
|
import net.hzvtc.service.EsResultRecordDetailsService;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/es/result/details")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EsResultRecordDetailsController {
|
||||||
|
private final EsResultRecordDetailsService esResultRecordDetailsService;
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/querySpeciesByParam")
|
||||||
|
public TableDataInfo querySpeciesListByParam(@RequestBody JSONObject params) {
|
||||||
|
params.put("isDeleted", 0);
|
||||||
|
return esResultRecordDetailsService.querySpeciesRecordListByParam(params, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/getSpeciesStatInfoByParam")
|
||||||
|
public JSONObject getSpeciesStatInfoByParam(@RequestBody JSONObject params) {
|
||||||
|
return esResultRecordDetailsService.getSpeciesStatInfo(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/querySpeciesByParamForApp")
|
||||||
|
public TableDataInfo querySpeciesListByParamForApp(@RequestBody JSONObject params) {
|
||||||
|
params.put("isDeleted", 0);
|
||||||
|
return esResultRecordDetailsService.querySpeciesRecordListByParam(params, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/querySpeciesByQuery")
|
||||||
|
public TableDataInfo querySpeciesListByQuery(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("isDeleted", 0);
|
||||||
|
return esResultRecordDetailsService.getProtectedAreaSpeciesByQuery(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/birdResultMedias")
|
||||||
|
public TableDataInfo getBirdResultMediasByParam(@RequestBody JSONObject params) {
|
||||||
|
return esResultRecordDetailsService.getResultMediaListByParam(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/delDocument/{ids}")
|
||||||
|
public AjaxResult deleteDocumentById(@PathVariable String ids) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.deleteDocumentById(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/updateResultRecord")
|
||||||
|
public AjaxResult updateResultRecord(@RequestBody JSONObject data) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.updateResultRecordInfoById(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateResultRecords")
|
||||||
|
public AjaxResult updateResultRecords(@RequestBody JSONObject data) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.updateResultRecordInfoByIds(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/birdAudioDailyStat")
|
||||||
|
public AjaxResult getBirdAudioDailyStatList(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("isDeleted", 0);
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getBirdAudioDailyStatListByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/birdDailyStat")
|
||||||
|
public AjaxResult getBirdDailyStatList(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("isDeleted", 0);
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getBirdDailyStatListByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/birdAudioMonthStat")
|
||||||
|
public AjaxResult getBirdAudioMonthStatList(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("isDeleted", 0);
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getBirdAudioMonthStatListByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/birdAudioYearStat")
|
||||||
|
public AjaxResult getBirdAudioYearStatList(@RequestBody JSONObject obj) {
|
||||||
|
obj.put("isDeleted", 0);
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getBirdAudioYearStatListByParam(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/listByParam")
|
||||||
|
public AjaxResult getListByParam(DeviceInfo deviceInfo) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getDeviceInfoListForPage(deviceInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/speciesListByParam")
|
||||||
|
public AjaxResult getSpeciesListByParam() {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getSpeciesListForSelect());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/mediaStatInfo")
|
||||||
|
public JSONObject getStatAmount() {
|
||||||
|
return esResultRecordDetailsService.getResultMediaStatByParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/speciesStatList")
|
||||||
|
public JSONObject getSpeciesMediaStatInfoByParam(@RequestBody JSONObject object) {
|
||||||
|
return esResultRecordDetailsService.getSpeciesStatListByParam(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/listForMap")
|
||||||
|
public List<DeviceInfo> getDeviceListForGisMap() {
|
||||||
|
return esResultRecordDetailsService.getDeviceInfoListForGisMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateDocumentInfoByParam")
|
||||||
|
public AjaxResult updateStatus(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.updateResultRecordInfoById(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateDocumentFavorite")
|
||||||
|
public AjaxResult updateFavorite(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.updateResultRecordInfoById(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateSpeciesInfo")
|
||||||
|
public AjaxResult updateSpeciesInfo(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.updateSpeciesInfo(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/deleteSpeciesInfo")
|
||||||
|
public AjaxResult deleteSpeciesInfo(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.deleteSpeciesInfo(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/addSpeciesInfo")
|
||||||
|
public AjaxResult addSpeciesInfo(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.addSpeciesInfo(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/favoriteList")
|
||||||
|
public AjaxResult getFavoriteMediaList(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getFavoriteMediaList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/listByItemId")
|
||||||
|
public AjaxResult getMediaListByItemId(@RequestBody JSONObject param) {
|
||||||
|
return AjaxResult.success(esResultRecordDetailsService.getMediaListByItemId(param));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package net.hzvtc.es.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.apache.ibatis.type.Alias;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Alias("EsResultRecordDetails")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
//@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class EsResultRecordDetails {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
|
||||||
|
private ZonedDateTime recordTime;
|
||||||
|
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
private Integer deviceType;
|
||||||
|
|
||||||
|
private String duid;
|
||||||
|
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
private String deptName;
|
||||||
|
|
||||||
|
private String deptLetter;
|
||||||
|
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
private Long mediaId;
|
||||||
|
|
||||||
|
private String mediaUrl;
|
||||||
|
|
||||||
|
private String metaUrl;
|
||||||
|
|
||||||
|
private int mediaType;
|
||||||
|
|
||||||
|
private Integer favorite;
|
||||||
|
|
||||||
|
private String memo;
|
||||||
|
|
||||||
|
private String tag;
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
private Long itemId;
|
||||||
|
|
||||||
|
private List<SpeciesItem> items;
|
||||||
|
|
||||||
|
private int isDeleted;
|
||||||
|
|
||||||
|
private String aiName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.hzvtc.es.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
|
||||||
|
public class EsResultRecordDetailsBak {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
|
||||||
|
private ZonedDateTime recordTime;
|
||||||
|
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
private Integer deviceType;
|
||||||
|
|
||||||
|
private String duid;
|
||||||
|
|
||||||
|
private Long speciesId;
|
||||||
|
|
||||||
|
private String speciesName;
|
||||||
|
|
||||||
|
private String scientificName;
|
||||||
|
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
private String habitat;
|
||||||
|
|
||||||
|
private double confidence;
|
||||||
|
|
||||||
|
private Long mediaId;
|
||||||
|
|
||||||
|
private String mediaUrl;
|
||||||
|
|
||||||
|
private String metaUrl;
|
||||||
|
|
||||||
|
private int mediaType;
|
||||||
|
|
||||||
|
private int recordType;
|
||||||
|
|
||||||
|
private Integer favorite;
|
||||||
|
|
||||||
|
private String birdIcon;
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
private int isDeleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package net.hzvtc.es.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.ibatis.type.Alias;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Alias("EsResultRecordDetailsVo")
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class EsResultRecordDetailsVo {
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
private LocalDateTime recordTime1;
|
||||||
|
|
||||||
|
private Long recordId;
|
||||||
|
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
private String deviceName;
|
||||||
|
|
||||||
|
private String duid;
|
||||||
|
|
||||||
|
private Long speciesId;
|
||||||
|
|
||||||
|
private String speciesName;
|
||||||
|
|
||||||
|
private String scientificName;
|
||||||
|
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
private int count;
|
||||||
|
|
||||||
|
private String habitat;
|
||||||
|
|
||||||
|
private double confidence;
|
||||||
|
|
||||||
|
private Long mediaId;
|
||||||
|
|
||||||
|
private String mediaUrl;
|
||||||
|
|
||||||
|
private String metaUrl;
|
||||||
|
|
||||||
|
private int mediaType;
|
||||||
|
|
||||||
|
private int recordType;
|
||||||
|
|
||||||
|
private Integer favorite;
|
||||||
|
|
||||||
|
private String birdIcon;
|
||||||
|
|
||||||
|
private int isDeleted;
|
||||||
|
|
||||||
|
public String getRecordTime() {
|
||||||
|
if (recordTime1 != null) {
|
||||||
|
// LocalDateTime localDateTime =
|
||||||
|
// ZonedDateTime zonedDateTime = recordTime1.atZone(ZoneId.of("Asia/Shanghai"));
|
||||||
|
|
||||||
|
// ZoneId systemDefault = ZoneId.systemDefault();
|
||||||
|
// return zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME);
|
||||||
|
// return recordTime1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"));
|
||||||
|
ZonedDateTime zonedDateTime = recordTime1.atZone(ZoneId.of("Asia/Shanghai"));
|
||||||
|
|
||||||
|
// 使用 DateTimeFormatter 格式化为 ISO 8601 格式的字符串
|
||||||
|
// return zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||||
|
ZonedDateTime utcDateTime = zonedDateTime.withZoneSameInstant(ZoneId.of("UTC"));
|
||||||
|
|
||||||
|
// 格式化为目标字符串
|
||||||
|
return utcDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package net.hzvtc.es.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import net.hzvtc.data.check.domain.MediaItem;
|
||||||
|
import net.hzvtc.device.domain.DeviceInfo;
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class EsResultRecordReportData {
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
|
||||||
|
private String recordTime;
|
||||||
|
|
||||||
|
private Long deviceId;
|
||||||
|
|
||||||
|
private DeviceInfo deviceInfo;
|
||||||
|
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
private String fullPath;
|
||||||
|
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
private Long mediaId;
|
||||||
|
|
||||||
|
private String mediaUrl;
|
||||||
|
|
||||||
|
private String metaUrl;
|
||||||
|
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
private int mediaType;
|
||||||
|
|
||||||
|
// private Integer favorite;
|
||||||
|
//
|
||||||
|
// private String memo;
|
||||||
|
//
|
||||||
|
// private String tag;
|
||||||
|
//
|
||||||
|
// private String version;
|
||||||
|
|
||||||
|
private Long itemId;
|
||||||
|
|
||||||
|
private List<SpeciesItem> items;
|
||||||
|
|
||||||
|
private MediaItem mediaItem;
|
||||||
|
|
||||||
|
public void convertFrom(EsResultRecordDetails data) {
|
||||||
|
setId(data.getId());
|
||||||
|
DateTimeFormatter fmDate = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
if (data.getRecordTime() != null) {
|
||||||
|
setRecordTime(data.getRecordTime().withZoneSameInstant(ZoneId.of("Asia/Shanghai")).format(fmDate));
|
||||||
|
} else {
|
||||||
|
setRecordTime("");
|
||||||
|
}
|
||||||
|
setDeptId(data.getDeptId());
|
||||||
|
setDeviceId(data.getDeviceId());
|
||||||
|
setMediaId(data.getMediaId());
|
||||||
|
setMediaUrl(data.getMediaUrl());
|
||||||
|
setMetaUrl(data.getMetaUrl());
|
||||||
|
setMediaType(data.getMediaType());
|
||||||
|
setItemId(data.getItemId());
|
||||||
|
|
||||||
|
if (mediaType == 2) {
|
||||||
|
if (metaUrl != null) {
|
||||||
|
String name = metaUrl.substring(metaUrl.lastIndexOf("/") + 1);
|
||||||
|
setFileName(name);
|
||||||
|
}
|
||||||
|
} else if (mediaType == 3) {
|
||||||
|
if (mediaUrl != null) {
|
||||||
|
String name = mediaUrl.substring(mediaUrl.lastIndexOf("/") + 1);
|
||||||
|
setFileName(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// setFavorite(data.getFavorite());
|
||||||
|
// setMemo(data.getMemo());
|
||||||
|
// setTag(data.getTag());
|
||||||
|
// setVersion(data.getVersion());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package net.hzvtc.es.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SpeciesItem {
|
||||||
|
private Long detailsId;
|
||||||
|
private Long recordId;
|
||||||
|
private Long speciesId;
|
||||||
|
private String speciesName;
|
||||||
|
private String scientificName;
|
||||||
|
private int count;
|
||||||
|
private int isValid;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package net.hzvtc.es.mapper;
|
||||||
|
|
||||||
|
import net.hzvtc.echart.domain.vo.BirdStatDataVo;
|
||||||
|
import net.hzvtc.es.domain.EsResultRecordDetailsVo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface EsResultRecordDetailsMapper {
|
||||||
|
List<EsResultRecordDetailsVo> getResultRecordListForElastic(Map<String, Object> param);
|
||||||
|
|
||||||
|
List<BirdStatDataVo> getDeviceListByParam(Map<String, Object> map);
|
||||||
|
|
||||||
|
List<BirdStatDataVo> getAreaBirdSpeciesByParam(Map<String, Object> map);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package net.hzvtc.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface EsRecordReportService {
|
||||||
|
JSONObject getBirdDailyReportStatByParam(JSONObject param);
|
||||||
|
|
||||||
|
// String getBirdDataStatFileByParam(JSONObject param);
|
||||||
|
|
||||||
|
String exportResultRecordListFileByParam(Map<String, Object> param);
|
||||||
|
|
||||||
|
String exportValidResultRecordListFileByParam(Map<String, Object> param);
|
||||||
|
|
||||||
|
// boolean sendBirdDataStatEmail(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getSpeciesWeeklyReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getSpeciesMediaListByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getResultRecordStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getResultRecordValidStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getHeatValidStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getCameraRecordStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getAreaStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getSpeciesTreeList();
|
||||||
|
|
||||||
|
JSONObject getSpeciesMultiStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getRecordDailyStatReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getRecordStatDateReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getValidStatDateReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getValidRAIReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getValidRNReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getValidPercentReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getValidRAIRNPtReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getRhythmReportByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getBirdPeriodStatByParam(JSONObject object);
|
||||||
|
|
||||||
|
JSONObject getSpeciesHabitatStatReportByParam(JSONObject param);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package net.hzvtc.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import net.hzvtc.common.core.page.TableDataInfo;
|
||||||
|
import net.hzvtc.device.domain.DeviceInfo;
|
||||||
|
import net.hzvtc.es.domain.EsResultRecordDetails;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface EsResultRecordDetailsService {
|
||||||
|
TableDataInfo querySpeciesRecordListByParam(JSONObject param, int type);
|
||||||
|
|
||||||
|
TableDataInfo getProtectedAreaSpeciesByQuery(JSONObject param);
|
||||||
|
|
||||||
|
TableDataInfo getResultMediaListByParam(JSONObject jsonObject);
|
||||||
|
|
||||||
|
int deleteDocumentById(String id);
|
||||||
|
|
||||||
|
void bulkAddResultRecordDocument();
|
||||||
|
|
||||||
|
int getMaxResultRecordId();
|
||||||
|
|
||||||
|
boolean updateResultRecordInfoById(JSONObject object);
|
||||||
|
|
||||||
|
boolean updateResultRecordInfoByIds(JSONObject object);
|
||||||
|
|
||||||
|
JSONObject getBirdAudioDailyStatListByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getBirdDailyStatListByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getBirdAudioMonthStatListByParam(JSONObject param);
|
||||||
|
|
||||||
|
JSONObject getBirdAudioYearStatListByParam(JSONObject param);
|
||||||
|
|
||||||
|
List<DeviceInfo> getDeviceInfoListForPage(DeviceInfo deviceInfo);
|
||||||
|
|
||||||
|
JSONArray getSpeciesListForSelect();
|
||||||
|
|
||||||
|
JSONObject getResultMediaStatByParam();
|
||||||
|
|
||||||
|
JSONObject getSpeciesStatListByParam(JSONObject param);
|
||||||
|
|
||||||
|
List<DeviceInfo> getDeviceInfoListForGisMap();
|
||||||
|
|
||||||
|
JSONObject getSpeciesStatInfo(JSONObject param);
|
||||||
|
|
||||||
|
boolean updateSpeciesInfo(JSONObject object);
|
||||||
|
|
||||||
|
|
||||||
|
boolean deleteSpeciesInfo(JSONObject object);
|
||||||
|
|
||||||
|
Long addSpeciesInfo(JSONObject object);
|
||||||
|
|
||||||
|
List<EsResultRecordDetails> getFavoriteMediaList(JSONObject param);
|
||||||
|
|
||||||
|
List<EsResultRecordDetails> getMediaListByItemId(JSONObject param);
|
||||||
|
|
||||||
|
// JSONObject getBirdDailyReportStatByParam(JSONObject param);
|
||||||
|
|
||||||
|
// String getBirdDataStatFileByParam(JSONObject param);
|
||||||
|
|
||||||
|
// boolean sendBirdDataStatEmail(JSONObject param);
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
package net.hzvtc.service.impl;
|
||||||
|
|
||||||
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||||
|
import co.elastic.clients.elasticsearch._types.SortOrder;
|
||||||
|
import co.elastic.clients.elasticsearch._types.aggregations.*;
|
||||||
|
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
|
||||||
|
import co.elastic.clients.elasticsearch.core.SearchRequest;
|
||||||
|
import co.elastic.clients.elasticsearch.core.SearchResponse;
|
||||||
|
import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||||
|
import co.elastic.clients.elasticsearch.core.search.TrackHits;
|
||||||
|
import co.elastic.clients.json.JsonData;
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.hzvtc.es.domain.EsResultRecordDetails;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CommonEsUtils {
|
||||||
|
public static JSONArray getSpeciesAggregateList(JSONObject param, String indexName, ElasticsearchClient elasticsearchClient, CalendarInterval calendarInterval) throws IOException {
|
||||||
|
BoolQuery.Builder boolQueryBuilder = createQuery(param);
|
||||||
|
// log.info("========111========{}", param);
|
||||||
|
SearchRequest searchRequest = SearchRequest.of(builder -> builder.index(indexName).size(0).query(q -> q.bool(boolQueryBuilder.build())).aggregations("by_speciesId", a -> a.terms(TermsAggregation.of(ta -> ta.field("species_id").size(10000))).aggregations("by_calendarInterval", b -> b.dateHistogram(DateHistogramAggregation.of(dh -> dh.field("record_time").calendarInterval(calendarInterval))).aggregations("sum_count_field", s -> s.sum(SumAggregation.of(sa -> sa.field("count")))))));
|
||||||
|
SearchResponse<Void> searchResponse = elasticsearchClient.search(searchRequest, Void.class);
|
||||||
|
Map<String, Aggregate> aggs = searchResponse.aggregations();
|
||||||
|
Aggregate bySpeciesAgg = aggs.get("by_speciesId");
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
|
|
||||||
|
bySpeciesAgg.lterms().buckets().array().forEach(speciesBucket -> {
|
||||||
|
Aggregate byCalendarInterval = speciesBucket.aggregations().get("by_calendarInterval");
|
||||||
|
byCalendarInterval.dateHistogram().buckets().array().forEach(hourBucket -> {
|
||||||
|
if (hourBucket.keyAsString() != null) {
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
LocalDateTime dateTime = LocalDateTime.parse(hourBucket.keyAsString(), formatter);
|
||||||
|
if (calendarInterval == CalendarInterval.Hour) {
|
||||||
|
item.put("hour", dateTime.getHour());
|
||||||
|
} else if (calendarInterval == CalendarInterval.Day) {
|
||||||
|
item.put("weekDay", dateTime.getDayOfWeek());
|
||||||
|
}
|
||||||
|
|
||||||
|
item.put("speciesId", speciesBucket.key());
|
||||||
|
Aggregate sumAgg = hourBucket.aggregations().get("sum_count_field");
|
||||||
|
item.put("count", (int) sumAgg.sum().value());
|
||||||
|
jsonArray.add(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return jsonArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EsResultRecordDetails getResultRecordDetailsByParam(JSONObject param, String indexName, ElasticsearchClient elasticsearchClient) throws IOException {
|
||||||
|
BoolQuery.Builder boolQueryBuilder = createQuery(param);
|
||||||
|
SearchResponse<EsResultRecordDetails> response = elasticsearchClient.search(s -> s.trackTotalHits(new TrackHits.Builder().enabled(true).build())//
|
||||||
|
.index(indexName).query(q -> q.bool(boolQueryBuilder.build()))//
|
||||||
|
.sort(sort -> sort.field(f -> f.field("favorite").order(SortOrder.Desc)))//
|
||||||
|
.sort(sort -> sort.field(f -> f.field("id").order(SortOrder.Desc)))//
|
||||||
|
// .source(sc -> sc
|
||||||
|
// .filter(f -> f
|
||||||
|
// .includes(Arrays.asList(sources))
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
.from(0).size(1), EsResultRecordDetails.class);
|
||||||
|
|
||||||
|
List<Hit<EsResultRecordDetails>> hitList = response.hits().hits();
|
||||||
|
|
||||||
|
for (Hit<EsResultRecordDetails> mapHit : hitList) {
|
||||||
|
return mapHit.source();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getSpeciesStatAmountByParam(JSONObject param, String indexName, ElasticsearchClient elasticsearchClient) throws IOException {
|
||||||
|
BoolQuery.Builder boolQueryBuilder = createQuery(param);
|
||||||
|
SearchRequest searchRequest = SearchRequest.of(builder -> builder.index(indexName).size(0).query(q -> q.bool(boolQueryBuilder.build())).aggregations("sum_count_field", a -> a.sum(SumAggregation.of(ta -> ta.field("count")))));
|
||||||
|
SearchResponse<Void> searchResponse = elasticsearchClient.search(searchRequest, Void.class);
|
||||||
|
Map<String, Aggregate> aggs = searchResponse.aggregations();
|
||||||
|
Aggregate sumAgg = aggs.get("sum_count_field");
|
||||||
|
return (long) sumAgg.sum().value();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static BoolQuery.Builder createQuery(JSONObject param) {
|
||||||
|
// log.info("==================param==========={}", param);
|
||||||
|
BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
|
||||||
|
|
||||||
|
// Long areaId = param.getLong("areaId");
|
||||||
|
// if (areaId != null) {
|
||||||
|
// boolQueryBuilder.must(m -> m.match(t -> t.field("area_id").query(areaId)));
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
Long speciesId = param.getLong("speciesId");
|
||||||
|
if (speciesId != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.nested(n -> n.path("items").query(q -> q.match(t -> t.field("items.species_id").query(speciesId)))));
|
||||||
|
// boolQueryBuilder.must(m -> m.match(t -> t.field("items.species_id").query(speciesId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long species_id = param.getLong("species_id");
|
||||||
|
if (species_id != null) {
|
||||||
|
// boolQueryBuilder.must(m -> m.nested(n -> n.path("items").query(q -> q.match(t -> t.field("items.species_id").query(speciesId)))));
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("species_id").query(species_id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deviceId = param.getLong("deviceId");
|
||||||
|
if (deviceId != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("device_id").query(deviceId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Long mediaId = param.getLong("mediaId");
|
||||||
|
if (mediaId != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("media_id").query(mediaId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer deviceType = param.getInteger("deviceType");
|
||||||
|
if (deviceType != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("device_type").query(deviceType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer favorite = param.getInteger("favorite");
|
||||||
|
if (favorite != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("favorite").query(favorite)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer checked = param.getInteger("checked");
|
||||||
|
if (checked != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("checked").query(checked)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String version = param.getString("version");
|
||||||
|
if (version != null && !version.isEmpty()) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("version").query(version)));
|
||||||
|
}
|
||||||
|
Long itemId = param.getLong("itemId");
|
||||||
|
if (itemId != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("item_id").query(itemId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String tag = param.getString("tag");
|
||||||
|
if (tag != null && !tag.isEmpty()) {
|
||||||
|
// log.info("============tag==========={}", tag);
|
||||||
|
// boolQueryBuilder.must(m -> m.fuzzy(fz -> fz.field("tag").value(tag).fuzziness("1")));
|
||||||
|
boolQueryBuilder.must(m -> m.matchPhrase(fz -> fz.field("tag").query(tag)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String memo = param.getString("memo");
|
||||||
|
if (memo != null && !memo.isEmpty()) {
|
||||||
|
// log.info("============memo==========={}", memo);
|
||||||
|
// boolQueryBuilder.must(m -> m.fuzzy(fz -> fz.field("memo").value(memo).fuzziness("1")));
|
||||||
|
boolQueryBuilder.must(m -> m.matchPhrase(fz -> fz.field("memo").query(memo)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String queryStr = param.getString("queryStr");
|
||||||
|
if (queryStr != null && !queryStr.trim().isEmpty()) {
|
||||||
|
boolQueryBuilder.should(s1 -> s1.match(fz -> fz.field("tag").query(queryStr))).should(s2 -> s2.match(fz -> fz.field("memo").query(queryStr))).minimumShouldMatch("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer isDeleted = param.getInteger("isDeleted");
|
||||||
|
if (isDeleted != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("is_deleted").query(isDeleted)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String dateFrom = param.getString("dateFrom");
|
||||||
|
if (dateFrom != null && !dateFrom.isEmpty()) {
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").gte(JsonData.of(dateFrom)).timeZone("+08:00")));
|
||||||
|
}
|
||||||
|
|
||||||
|
String dateTo = param.getString("dateTo");
|
||||||
|
if (dateTo != null && !dateTo.isEmpty()) {
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").lte(JsonData.of(dateTo)).timeZone("+08:00")));
|
||||||
|
}
|
||||||
|
|
||||||
|
String startYear = param.getString("startYear");
|
||||||
|
if (startYear != null && !startYear.isEmpty()) {
|
||||||
|
startYear = startYear + "-01-01";
|
||||||
|
String finalStartYear = startYear;
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").gte(JsonData.of(finalStartYear)).timeZone("+08:00")));
|
||||||
|
}
|
||||||
|
|
||||||
|
String endYear = param.getString("endYear");
|
||||||
|
if (endYear != null && !endYear.isEmpty()) {
|
||||||
|
endYear = endYear + "-12-31";
|
||||||
|
String finalEndYear = endYear;
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").lte(JsonData.of(finalEndYear))));
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer recordType = param.getInteger("recordType");
|
||||||
|
if (recordType != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("record_type").query(recordType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String calcDate = param.getString("calcDate");
|
||||||
|
if (calcDate != null && !calcDate.isEmpty()) {
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").lte(JsonData.of(calcDate)).gte(JsonData.of(calcDate)).timeZone("+08:00")));
|
||||||
|
}
|
||||||
|
|
||||||
|
String calcYear = param.getString("calcYear");
|
||||||
|
if (calcYear != null && !calcYear.isEmpty()) {
|
||||||
|
String from = calcYear + "-01-01";
|
||||||
|
String to = calcYear + "-12-31";
|
||||||
|
boolQueryBuilder.must(m -> m.range(v -> v.field("record_time").format("yyyy-MM-dd").lte(JsonData.of(to)).gte(JsonData.of(from)).timeZone("+08:00")));
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONArray array = param.getJSONArray("exSpeciesIds");
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
// log.info("==========exSpeciesIds=========={}", array);
|
||||||
|
createExcludeArrayQuery("species_id", null, array, boolQueryBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
array = param.getJSONArray("ids");
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
createExcludeArrayQuery("items", "items.species_id", array, boolQueryBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
array = param.getJSONArray("speciesIds");
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
createIncludeNestArrayQuery("items", "items.species_id", array, boolQueryBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
array = param.getJSONArray("speciesInIds");
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
createIncludeArrayQuery("species_id", array, boolQueryBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer mediaType = param.getInteger("mediaType");
|
||||||
|
if (mediaType != null) {
|
||||||
|
boolQueryBuilder.must(m -> m.match(t -> t.field("media_type").query(mediaType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
String mediaTypes = param.getString("mediaTypes");
|
||||||
|
if (mediaTypes != null && !mediaTypes.isEmpty()) {
|
||||||
|
String[] arr = mediaTypes.split(",");
|
||||||
|
List<String> terms = Arrays.asList(arr);
|
||||||
|
List<FieldValue> fieldValues = terms.stream().map(FieldValue::of).collect(Collectors.toList());
|
||||||
|
boolQueryBuilder.must(m -> m.terms(t -> t.field("media_type").terms(tq -> tq.value(fieldValues))));
|
||||||
|
}
|
||||||
|
|
||||||
|
array = param.getJSONArray("deviceIds");
|
||||||
|
// log.info("=============device ids==={}", array);
|
||||||
|
createIncludeArrayQuery("device_id", array, boolQueryBuilder);
|
||||||
|
|
||||||
|
array = param.getJSONArray("inDocIds");
|
||||||
|
createIncludeArrayQuery("id", array, boolQueryBuilder);
|
||||||
|
// if (array != null && !array.isEmpty()) {
|
||||||
|
// List<Long> list = new ArrayList<>();
|
||||||
|
// for (Object id : array) {
|
||||||
|
// list.add((Long) id);
|
||||||
|
// }
|
||||||
|
// List<FieldValue> fieldValues = list.stream().map(FieldValue::of).collect(Collectors.toList());
|
||||||
|
// boolQueryBuilder.mustNot(m -> m.terms(t -> t.field("device_id").terms(tq -> tq.value(fieldValues))));
|
||||||
|
// }
|
||||||
|
// log.info("============={}", boolQueryBuilder);
|
||||||
|
return boolQueryBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createIncludeArrayQuery(String fieldName, JSONArray array, BoolQuery.Builder boolQueryBuilder) {
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
List<Long> list = new ArrayList<>();
|
||||||
|
for (Object id : array) {
|
||||||
|
list.add(Long.parseLong(id.toString()));
|
||||||
|
}
|
||||||
|
List<FieldValue> fieldValues = list.stream().map(FieldValue::of).collect(Collectors.toList());
|
||||||
|
boolQueryBuilder.must(m -> m.terms(t -> t.field(fieldName).terms(tq -> tq.value(fieldValues))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createIncludeNestArrayQuery(String fieldName, String nestedName, JSONArray array, BoolQuery.Builder boolQueryBuilder) {
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
List<Long> list = new ArrayList<>();
|
||||||
|
for (Object id : array) {
|
||||||
|
list.add(Long.parseLong(id.toString()));
|
||||||
|
}
|
||||||
|
List<FieldValue> fieldValues = list.stream().map(FieldValue::of).collect(Collectors.toList());
|
||||||
|
if (nestedName != null && !nestedName.isEmpty()) {
|
||||||
|
boolQueryBuilder.must(mn -> mn.nested(n -> n.path(fieldName).query(nq -> nq.terms(t -> t.field(nestedName).terms(tq -> tq.value(fieldValues))))));
|
||||||
|
} else {
|
||||||
|
boolQueryBuilder.must(m -> m.terms(t -> t.field(fieldName).terms(tq -> tq.value(fieldValues))));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createExcludeArrayQuery(String fieldName, String nestedName, JSONArray array, BoolQuery.Builder boolQueryBuilder) {
|
||||||
|
if (array != null && !array.isEmpty()) {
|
||||||
|
List<Long> list = new ArrayList<>();
|
||||||
|
for (Object id : array) {
|
||||||
|
list.add((Long) id);
|
||||||
|
}
|
||||||
|
List<FieldValue> fieldValues = list.stream().map(FieldValue::of).collect(Collectors.toList());
|
||||||
|
if (nestedName != null && !nestedName.isEmpty()) {
|
||||||
|
boolQueryBuilder.mustNot(mn -> mn.nested(n -> n.path(fieldName).query(nq -> nq.terms(t -> t.field(nestedName).terms(tq -> tq.value(fieldValues))))));
|
||||||
|
} else {
|
||||||
|
boolQueryBuilder.mustNot(m -> m.terms(t -> t.field(fieldName).terms(tq -> tq.value(fieldValues))));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+1217
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,126 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.ChainLengthItem;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.util.GeoUtils;
|
||||||
|
import net.hzvtc.stat.util.StatisticsUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 chainLength(chain) / chainLengths(chainDB) 的 Java 实现。
|
||||||
|
*
|
||||||
|
* <p>算法:
|
||||||
|
* <ol>
|
||||||
|
* <li>把每条足迹链转 UTM 米。</li>
|
||||||
|
* <li>计算相邻点的距离(km),若 > 100km 视为跨区清零。</li>
|
||||||
|
* <li>累加得到总链长(km)。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>chainLengths 按 Species/Line 聚合,额外给出每物种平均步长(m)与推算步数。</p>
|
||||||
|
*/
|
||||||
|
public final class ChainLength {
|
||||||
|
|
||||||
|
private ChainLength() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单条足迹链长度(km)。
|
||||||
|
*
|
||||||
|
* @param chain 足迹链
|
||||||
|
* @return 链长(km)
|
||||||
|
*/
|
||||||
|
public static double chainLength(List<FootprintPoint> chain) {
|
||||||
|
if (chain == null || chain.size() < 2) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
double[][] xy = GeoUtils.wgs84ToUtmAuto(chain, p -> p.getLon(), p -> p.getLat());
|
||||||
|
double totalKm = 0.0;
|
||||||
|
for (int i = 1; i < xy.length; i++) {
|
||||||
|
double dx = (xy[i][0] - xy[i - 1][0]) / 1000.0;
|
||||||
|
double dy = (xy[i][1] - xy[i - 1][1]) / 1000.0;
|
||||||
|
double d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
// 跨区 (>100 km) 清零
|
||||||
|
totalKm += d > 100.0 ? 0.0 : d;
|
||||||
|
}
|
||||||
|
return Math.round(totalKm * 1000.0) / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多物种多链的链长聚合(每物种一行)。
|
||||||
|
*/
|
||||||
|
public static List<ChainLengthItem> chainLengths(List<ChainRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
// 按 Species 分组
|
||||||
|
Map<String, List<ChainRecord>> bySpecies = new HashMap<>();
|
||||||
|
for (ChainRecord r : records) {
|
||||||
|
bySpecies.computeIfAbsent(r.getSpecies(), k -> new ArrayList<>()).add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainLengthItem> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<ChainRecord>> e : bySpecies.entrySet()) {
|
||||||
|
String species = e.getKey();
|
||||||
|
List<ChainRecord> sp = e.getValue();
|
||||||
|
|
||||||
|
// 按 Line 分组
|
||||||
|
Map<Integer, List<ChainRecord>> byLine = new HashMap<>();
|
||||||
|
for (ChainRecord r : sp) {
|
||||||
|
byLine.computeIfAbsent(r.getLine(), k -> new ArrayList<>()).add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Double> distances = new ArrayList<>();
|
||||||
|
List<Double> steps = new ArrayList<>();
|
||||||
|
for (List<ChainRecord> lineRecs : byLine.values()) {
|
||||||
|
List<FootprintPoint> chain = new ArrayList<>(lineRecs.size());
|
||||||
|
for (ChainRecord r : lineRecs) {
|
||||||
|
chain.add(new FootprintPoint(r.getLon(), r.getLat(), r.getDate()));
|
||||||
|
}
|
||||||
|
double distKm = chainLength(chain);
|
||||||
|
distances.add(distKm);
|
||||||
|
|
||||||
|
// 平均步长(m):相邻点距离(m)求均值
|
||||||
|
double[][] xy = GeoUtils.wgs84ToUtmAuto(chain, p -> p.getLon(), p -> p.getLat());
|
||||||
|
if (xy.length >= 2) {
|
||||||
|
double sum = 0.0;
|
||||||
|
int cnt = 0;
|
||||||
|
for (int i = 1; i < xy.length; i++) {
|
||||||
|
double dx = xy[i][0] - xy[i - 1][0];
|
||||||
|
double dy = xy[i][1] - xy[i - 1][1];
|
||||||
|
double d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (d > 0 && d <= 100000) {
|
||||||
|
sum += d;
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cnt > 0) {
|
||||||
|
steps.add(sum / cnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] dArr = distances.stream().mapToDouble(Double::doubleValue).toArray();
|
||||||
|
double[] sArr = steps.stream().mapToDouble(Double::doubleValue).toArray();
|
||||||
|
double meanDist = StatisticsUtils.mean(dArr);
|
||||||
|
double sdDist = StatisticsUtils.sd(dArr);
|
||||||
|
double meanStep = sArr.length > 0 ? StatisticsUtils.mean(sArr) : 0.0;
|
||||||
|
long stepNo = meanStep > 0 ? (long) Math.floor(meanDist * 1000.0 / meanStep * 30.0) : 0L;
|
||||||
|
|
||||||
|
result.add(new ChainLengthItem(
|
||||||
|
species,
|
||||||
|
Math.round(meanDist * 1000.0) / 1000.0,
|
||||||
|
Math.round(sdDist * 1000.0) / 1000.0,
|
||||||
|
Math.round(meanStep * 10.0) / 10.0,
|
||||||
|
stepNo,
|
||||||
|
dArr.length
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.DensitySeries;
|
||||||
|
import net.hzvtc.stat.util.DensityUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 dailyRhythm(x) 的 Java 实现:返回 24 小时活动节律的两条 KDE 曲线(bw=0.3 细线 / bw=2 粗线)。
|
||||||
|
*
|
||||||
|
* <p>算法:
|
||||||
|
* <ol>
|
||||||
|
* <li>把每个 Time 字符串 (HH:mm:ss) 转为相对 0 点的秒数,再除以 3600 得到 0-24 小时浮点值。</li>
|
||||||
|
* <li>在 [0, 24] 区间分别用 bw=0.3 与 bw=2 计算高斯核密度。</li>
|
||||||
|
* <li>返回两条 {@link DensitySeries}(前端用 ECharts 画)。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class DailyRhythm {
|
||||||
|
|
||||||
|
private DailyRhythm() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认采样点数(与 R density 默认 n=512 一致)。 */
|
||||||
|
private static final int N = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param trapresult 数据(含 Time 字段)
|
||||||
|
* @return 两条曲线:[bw=0.3, bw=2]
|
||||||
|
*/
|
||||||
|
public static List<DensitySeries> dailyRhythm(List<CameraPoint> trapresult) {
|
||||||
|
double[] hours = toHourArray(trapresult);
|
||||||
|
|
||||||
|
DensitySeries fine = DensityUtils.gaussianKde(hours, 0.3, N, 0.0, 24.0, "bw=0.3");
|
||||||
|
DensitySeries coarse = DensityUtils.gaussianKde(hours, 2.0, N, 0.0, 24.0, "bw=2");
|
||||||
|
|
||||||
|
List<DensitySeries> list = new ArrayList<>(2);
|
||||||
|
list.add(fine);
|
||||||
|
list.add(coarse);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅返回单个 bw 的曲线(便于前端切换不同精度)。 */
|
||||||
|
public static DensitySeries dailyRhythm(List<CameraPoint> trapresult, double bw) {
|
||||||
|
double[] hours = toHourArray(trapresult);
|
||||||
|
return DensityUtils.gaussianKde(hours, bw, N, 0.0, 24.0, "bw=" + bw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 Time 字符串解析为 0-24 的小时浮点数;不可解析的值会被跳过。
|
||||||
|
*/
|
||||||
|
private static double[] toHourArray(List<CameraPoint> trapresult) {
|
||||||
|
List<Double> list = new ArrayList<>();
|
||||||
|
for (CameraPoint p : trapresult) {
|
||||||
|
String t = p.getTime();
|
||||||
|
if (t == null || t.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] parts = t.split(":");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int h = Integer.parseInt(parts[0]);
|
||||||
|
int m = Integer.parseInt(parts[1]);
|
||||||
|
double s = parts.length > 2 ? Double.parseDouble(parts[2]) : 0.0;
|
||||||
|
double h24 = h + m / 60.0 + s / 3600.0;
|
||||||
|
if (h24 < 0 || h24 > 24) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list.add(h24);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// 跳过无法解析的 Time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double[] arr = new double[list.size()];
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.RasterResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 种群密度估计:基于随机森林的物种分布模型(SDM),输出栅格化出现概率图。
|
||||||
|
*
|
||||||
|
* <p>结构对应"随机森林 SDM"的通用流程(cameratrapR 之外的扩展分析):
|
||||||
|
* <ol>
|
||||||
|
* <li>按相机点位聚合照片总数(红外相机点位 + 积累照片总数作为出现强度)。</li>
|
||||||
|
* <li>以点位分布范围为边界(外扩 3 格)建立规则网格。</li>
|
||||||
|
* <li>每格特征:经度、纬度、高斯核照片密度(KDE)、到最近相机距离。</li>
|
||||||
|
* <li>训练样本:相机所在格 = 出现(按照片数加权复制);远离相机的背景格 = 未出现(随机抽样)。</li>
|
||||||
|
* <li>Bootstrap 随机森林分类器(随机特征子集 + Gini 分裂,叶结点存正样比例),
|
||||||
|
* 对每个格子预测出现概率,值域 [0, 1]。</li>
|
||||||
|
* </ol>
|
||||||
|
* 每个物种的照片分布不同,因此每个物种得到各自的密度栅格。</p>
|
||||||
|
*/
|
||||||
|
public final class DensityEstimate {
|
||||||
|
|
||||||
|
private DensityEstimate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 栅格总格数上限(超过则自动放粗格宽)。 */
|
||||||
|
private static final int MAX_CELLS = 200000;
|
||||||
|
|
||||||
|
/** KDE 带宽(格宽倍数)。 */
|
||||||
|
private static final double BANDWIDTH_CELLS = 3.0;
|
||||||
|
|
||||||
|
/** 每个出现格按照片数复制的次数上限。 */
|
||||||
|
private static final int MAX_WEIGHT = 10;
|
||||||
|
|
||||||
|
/** 背景未出现格数量(出现样本数的倍数)。 */
|
||||||
|
private static final int BACKGROUND_RATIO = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data 相机记录(lon/lat/groupSize/species)
|
||||||
|
* @param cellSize 格宽(度),≤0 时自动按范围选择
|
||||||
|
* @param nTree 随机森林树数
|
||||||
|
* @param seed 随机种子
|
||||||
|
* @return 栅格化出现概率
|
||||||
|
*/
|
||||||
|
public static RasterResult densityEstimate(List<CameraPoint> data, double cellSize,
|
||||||
|
int nTree, long seed) {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return new RasterResult(0, 0, 0, 0, 0, new double[0], 0, 0, 0);
|
||||||
|
}
|
||||||
|
Random rng = new Random(seed);
|
||||||
|
|
||||||
|
// 1) 聚合相机点位:key(lon,lat) → 照片总数
|
||||||
|
Map<String, double[]> cameras = new HashMap<>();
|
||||||
|
long photoTotal = 0;
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
String key = p.getLon() + "," + p.getLat();
|
||||||
|
double[] agg = cameras.get(key);
|
||||||
|
if (agg == null) {
|
||||||
|
cameras.put(key, new double[]{p.getLon(), p.getLat(), Math.max(1, p.getGroupSize())});
|
||||||
|
} else {
|
||||||
|
agg[2] += Math.max(1, p.getGroupSize());
|
||||||
|
}
|
||||||
|
photoTotal += Math.max(1, p.getGroupSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 边界 + 网格(外扩 3 格)
|
||||||
|
double minLon = Double.POSITIVE_INFINITY, maxLon = Double.NEGATIVE_INFINITY;
|
||||||
|
double minLat = Double.POSITIVE_INFINITY, maxLat = Double.NEGATIVE_INFINITY;
|
||||||
|
for (double[] c : cameras.values()) {
|
||||||
|
minLon = Math.min(minLon, c[0]);
|
||||||
|
maxLon = Math.max(maxLon, c[0]);
|
||||||
|
minLat = Math.min(minLat, c[1]);
|
||||||
|
maxLat = Math.max(maxLat, c[1]);
|
||||||
|
}
|
||||||
|
if (cellSize <= 0) {
|
||||||
|
cellSize = Math.max(maxLon - minLon, maxLat - minLat) / 99.0;
|
||||||
|
}
|
||||||
|
double span = Math.max(Math.max(maxLon - minLon, maxLat - minLat), cellSize);
|
||||||
|
while (Math.ceil((span + 6 * cellSize) / cellSize)
|
||||||
|
* Math.ceil((span + 6 * cellSize) / cellSize) > MAX_CELLS) {
|
||||||
|
cellSize *= 1.5;
|
||||||
|
}
|
||||||
|
double pad = 3 * cellSize;
|
||||||
|
double x0 = Math.floor((minLon - pad) / cellSize) * cellSize;
|
||||||
|
double y0 = Math.floor((minLat - pad) / cellSize) * cellSize;
|
||||||
|
double x1 = Math.ceil((maxLon + pad) / cellSize) * cellSize;
|
||||||
|
double y1 = Math.ceil((maxLat + pad) / cellSize) * cellSize;
|
||||||
|
int nx = Math.max(2, (int) Math.round((x1 - x0) / cellSize));
|
||||||
|
int ny = Math.max(2, (int) Math.round((y1 - y0) / cellSize));
|
||||||
|
|
||||||
|
// 3) 相机 → 格索引(去重后同格合并权重)
|
||||||
|
Map<Integer, Double> presenceWeight = new HashMap<>();
|
||||||
|
double[] camLon = new double[cameras.size()];
|
||||||
|
double[] camLat = new double[cameras.size()];
|
||||||
|
double[] camCount = new double[cameras.size()];
|
||||||
|
int ci = 0;
|
||||||
|
for (double[] c : cameras.values()) {
|
||||||
|
camLon[ci] = c[0];
|
||||||
|
camLat[ci] = c[1];
|
||||||
|
camCount[ci] = c[2];
|
||||||
|
ci++;
|
||||||
|
int col = clamp((int) Math.floor((c[0] - x0) / cellSize), 0, nx - 1);
|
||||||
|
int row = clamp((int) Math.floor((c[1] - y0) / cellSize), 0, ny - 1);
|
||||||
|
presenceWeight.merge(row * nx + col, c[2], Double::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
double bandwidth = BANDWIDTH_CELLS * cellSize;
|
||||||
|
|
||||||
|
// 4) 每格特征:lon / lat / KDE / 最近相机距离
|
||||||
|
double[][] features = new double[nx * ny][];
|
||||||
|
for (int r = 0; r < ny; r++) {
|
||||||
|
double lat = y0 + (r + 0.5) * cellSize;
|
||||||
|
for (int c = 0; c < nx; c++) {
|
||||||
|
double lon = x0 + (c + 0.5) * cellSize;
|
||||||
|
double kde = 0;
|
||||||
|
double nearest = Double.POSITIVE_INFINITY;
|
||||||
|
for (int k = 0; k < camLon.length; k++) {
|
||||||
|
double d = Math.hypot(lon - camLon[k], lat - camLat[k]);
|
||||||
|
nearest = Math.min(nearest, d);
|
||||||
|
double z = d / bandwidth;
|
||||||
|
if (z < 4) {
|
||||||
|
kde += camCount[k] * Math.exp(-0.5 * z * z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features[r * nx + c] = new double[]{lon, lat, kde, nearest};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 训练样本:出现格(按权重复制)+ 背景未出现格(抽样)
|
||||||
|
List<double[]> trainX = new ArrayList<>();
|
||||||
|
List<Integer> trainY = new ArrayList<>();
|
||||||
|
for (Map.Entry<Integer, Double> e : presenceWeight.entrySet()) {
|
||||||
|
int reps = clamp((int) Math.round(1 + Math.log1p(e.getValue())), 1, MAX_WEIGHT);
|
||||||
|
for (int i = 0; i < reps; i++) {
|
||||||
|
trainX.add(features[e.getKey()]);
|
||||||
|
trainY.add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Integer> background = new ArrayList<>();
|
||||||
|
for (int idx = 0; idx < nx * ny; idx++) {
|
||||||
|
if (!presenceWeight.containsKey(idx) && features[idx][3] > bandwidth) {
|
||||||
|
background.add(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int nNeg = Math.min(background.size(), trainY.size() * BACKGROUND_RATIO);
|
||||||
|
for (int i = 0; i < nNeg; i++) {
|
||||||
|
int idx = background.get(rng.nextInt(background.size()));
|
||||||
|
trainX.add(features[idx]);
|
||||||
|
trainY.add(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部格子都靠近相机(无背景样本)→ 用最远的 1/4 格子做未出现样本
|
||||||
|
if (nNeg == 0 && !background.isEmpty()) {
|
||||||
|
background.sort((a, b) -> Double.compare(features[b][3], features[a][3]));
|
||||||
|
for (int i = 0; i < Math.max(1, background.size() / 4); i++) {
|
||||||
|
trainX.add(features[background.get(i)]);
|
||||||
|
trainY.add(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) 随机森林训练 + 全格预测
|
||||||
|
double[] values = new double[nx * ny];
|
||||||
|
if (trainY.isEmpty() || nNeg == 0) {
|
||||||
|
// 退化:无有效负样本时直接用 KDE 归一化概率
|
||||||
|
double maxKde = 1e-12;
|
||||||
|
for (double[] f : features) {
|
||||||
|
maxKde = Math.max(maxKde, f[2]);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
values[i] = round4(features[i][2] / maxKde);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RandomForestClf rf = new RandomForestClf(Math.max(10, nTree), rng);
|
||||||
|
rf.fit(trainX, toIntArray(trainY));
|
||||||
|
double maxVal = 1e-12;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
values[i] = round4(rf.predictProb(features[i]));
|
||||||
|
maxVal = Math.max(maxVal, values[i]);
|
||||||
|
}
|
||||||
|
if (maxVal > 0) {
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
values[i] = round4(values[i] / maxVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double maxValue = 0;
|
||||||
|
for (double v : values) {
|
||||||
|
maxValue = Math.max(maxValue, v);
|
||||||
|
}
|
||||||
|
return new RasterResult(nx, ny, x0, y0, round6(cellSize), values, maxValue,
|
||||||
|
cameras.size(), photoTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int v, int lo, int hi) {
|
||||||
|
return Math.max(lo, Math.min(hi, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round4(double v) {
|
||||||
|
return Math.round(v * 10000.0) / 10000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round6(double v) {
|
||||||
|
return Math.round(v * 1000000.0) / 1000000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] toIntArray(List<Integer> list) {
|
||||||
|
int[] a = new int[list.size()];
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
a[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// 随机森林分类器(Bootstrap + 随机特征子集 + Gini 二叉树,叶存正样比例)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
static final class RandomForestClf {
|
||||||
|
|
||||||
|
private final List<ClsTree> trees = new ArrayList<>();
|
||||||
|
private final Random rng;
|
||||||
|
|
||||||
|
RandomForestClf(int nTree, Random rng) {
|
||||||
|
this.nTreeWanted = nTree;
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final int nTreeWanted;
|
||||||
|
|
||||||
|
void fit(List<double[]> x, int[] y) {
|
||||||
|
int n = x.size();
|
||||||
|
int nFeatures = x.get(0).length;
|
||||||
|
int subset = Math.max(1, (int) Math.round(Math.sqrt(nFeatures)));
|
||||||
|
for (int t = 0; t < nTreeWanted; t++) {
|
||||||
|
List<double[]> bx = new ArrayList<>(n);
|
||||||
|
int[] by = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
int idx = rng.nextInt(n);
|
||||||
|
bx.add(x.get(idx));
|
||||||
|
by[i] = y[idx];
|
||||||
|
}
|
||||||
|
ClsTree tree = new ClsTree();
|
||||||
|
tree.root = tree.build(bx, by, 0, subset, rng);
|
||||||
|
trees.add(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double predictProb(double[] x) {
|
||||||
|
if (trees.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double s = 0;
|
||||||
|
for (ClsTree t : trees) {
|
||||||
|
s += t.predict(x);
|
||||||
|
}
|
||||||
|
return s / trees.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gini 分类树:叶结点返回正样本比例。 */
|
||||||
|
static final class ClsTree {
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
|
||||||
|
static final class Node {
|
||||||
|
int featureIdx = -1;
|
||||||
|
double threshold;
|
||||||
|
Node left;
|
||||||
|
Node right;
|
||||||
|
double positiveRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node build(List<double[]> x, int[] y, int depth, int subset, Random rng) {
|
||||||
|
Node node = new Node();
|
||||||
|
int n = x.size();
|
||||||
|
int pos = 0;
|
||||||
|
for (int v : y) {
|
||||||
|
pos += v == 1 ? 1 : 0;
|
||||||
|
}
|
||||||
|
node.positiveRate = n == 0 ? 0 : (double) pos / n;
|
||||||
|
|
||||||
|
if (depth >= 8 || n < 4 || pos == 0 || pos == n) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nFeatures = x.get(0).length;
|
||||||
|
double parentGini = gini(pos, n - pos);
|
||||||
|
double bestGain = 1e-9;
|
||||||
|
int bestFeat = -1;
|
||||||
|
double bestThr = 0;
|
||||||
|
|
||||||
|
for (int f = 0; f < nFeatures; f++) {
|
||||||
|
if (nFeatures > subset && rng.nextBoolean()) {
|
||||||
|
continue; // 随机特征子集(近似 sqrt(p))
|
||||||
|
}
|
||||||
|
double lo = Double.POSITIVE_INFINITY, hi = Double.NEGATIVE_INFINITY;
|
||||||
|
for (double[] row : x) {
|
||||||
|
lo = Math.min(lo, row[f]);
|
||||||
|
hi = Math.max(hi, row[f]);
|
||||||
|
}
|
||||||
|
if (lo >= hi) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int k = 1; k <= 3; k++) { // 3 个候选分裂点
|
||||||
|
double thr = lo + (hi - lo) * k / 4.0;
|
||||||
|
int lPos = 0, lN = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (x.get(i)[f] <= thr) {
|
||||||
|
lN++;
|
||||||
|
lPos += y[i] == 1 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int rPos = pos - lPos, rN = n - lN;
|
||||||
|
if (lN == 0 || rN == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double gain = parentGini
|
||||||
|
- (lN * gini(lPos, lN - lPos) + rN * gini(rPos, rN - rPos)) / n;
|
||||||
|
if (gain > bestGain) {
|
||||||
|
bestGain = gain;
|
||||||
|
bestFeat = f;
|
||||||
|
bestThr = thr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestFeat < 0) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
node.featureIdx = bestFeat;
|
||||||
|
node.threshold = bestThr;
|
||||||
|
|
||||||
|
List<double[]> lx = new ArrayList<>();
|
||||||
|
List<Integer> ly = new ArrayList<>();
|
||||||
|
List<double[]> rx = new ArrayList<>();
|
||||||
|
List<Integer> ry = new ArrayList<>();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (x.get(i)[bestFeat] <= bestThr) {
|
||||||
|
lx.add(x.get(i));
|
||||||
|
ly.add(y[i]);
|
||||||
|
} else {
|
||||||
|
rx.add(x.get(i));
|
||||||
|
ry.add(y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.left = build(lx, toArr(ly), depth + 1, subset, rng);
|
||||||
|
node.right = build(rx, toArr(ry), depth + 1, subset, rng);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
double predict(double[] x) {
|
||||||
|
Node n = root;
|
||||||
|
while (n.featureIdx >= 0) {
|
||||||
|
n = x[n.featureIdx] <= n.threshold ? n.left : n.right;
|
||||||
|
}
|
||||||
|
return n.positiveRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double gini(int a, int b) {
|
||||||
|
int n = a + b;
|
||||||
|
if (n == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double pa = (double) a / n;
|
||||||
|
double pb = (double) b / n;
|
||||||
|
return 1.0 - pa * pa - pb * pb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] toArr(List<Integer> list) {
|
||||||
|
int[] a = new int[list.size()];
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
a[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.util.GeoUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 geo2proj 的 Java 重写:WGS84 经纬度 → UTM 米。
|
||||||
|
*
|
||||||
|
* <p>实现与 R 完全等价:
|
||||||
|
* <ul>
|
||||||
|
* <li>若 geo/proj 未指定,使用 "+proj=longlat +datum=WGS84" 与按经度均值推算的 UTM 区号。</li>
|
||||||
|
* <li>单位由「度」转为「米」。</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>本类使用 {@link GeoUtils} 手写的 Transverse Mercator(Kruger)公式实现,
|
||||||
|
* 不依赖 Proj4 / GeoTools 等第三方。</p>
|
||||||
|
*/
|
||||||
|
public final class Geo2Proj {
|
||||||
|
|
||||||
|
private Geo2Proj() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换 CameraPoint 列表。
|
||||||
|
*
|
||||||
|
* @return 每个元素为 [easting, northing],长度与入参相同。
|
||||||
|
*/
|
||||||
|
public static double[][] convert(List<CameraPoint> data) {
|
||||||
|
return GeoUtils.wgs84ToUtmAuto(data, p -> p.getLon(), p -> p.getLat());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转换 FootprintPoint 列表。 */
|
||||||
|
public static double[][] convertFootprint(List<FootprintPoint> data) {
|
||||||
|
return GeoUtils.wgs84ToUtmAuto(data, p -> p.getLon(), p -> p.getLat());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转换并以 DTO 形式返回,便于序列化。 */
|
||||||
|
public static List<UtmPoint> convertToDto(List<CameraPoint> data) {
|
||||||
|
double[][] xy = convert(data);
|
||||||
|
List<UtmPoint> out = new ArrayList<>(xy.length);
|
||||||
|
for (double[] v : xy) {
|
||||||
|
out.add(new UtmPoint(v[0], v[1]));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 简化 DTO:UTM 米坐标点。 */
|
||||||
|
@lombok.Data
|
||||||
|
@lombok.AllArgsConstructor
|
||||||
|
public static class UtmPoint {
|
||||||
|
private double easting;
|
||||||
|
private double northing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.HabitatAreaResult;
|
||||||
|
import net.hzvtc.stat.dto.RasterResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 栖息地面积分析:在种群密度栅格上确定最优阈值,输出适宜生境二值栅格。
|
||||||
|
*
|
||||||
|
* <p>阈值确定(对应 R 包中的物种适宜阈值思想,每个物种因密度栅格不同而阈值不同):
|
||||||
|
* <ul>
|
||||||
|
* <li>默认 max-Kappa 法:以相机所在格为"出现"样点、背景抽样格为"未出现"样点,
|
||||||
|
* 遍历密度分位数候选阈值,取 Cohen's Kappa 最大者;</li>
|
||||||
|
* <li>无出现样点时退化用分位数法(P75);</li>
|
||||||
|
* <li>前端可显式传阈值(method = manual)覆盖自动计算。</li>
|
||||||
|
* </ul>
|
||||||
|
* 输出二值栅格:阈值上(≥ 阈值)= 适宜生境(1),阈值下 = 不适宜(0),
|
||||||
|
* 面积按格数 × 格面积(经纬度格宽随纬度做 cos 校正)累加。</p>
|
||||||
|
*/
|
||||||
|
public final class HabitatAnalysis {
|
||||||
|
|
||||||
|
private HabitatAnalysis() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 赤道 1 度纬距(km)。 */
|
||||||
|
private static final double KM_PER_DEG = 111.32;
|
||||||
|
|
||||||
|
/** Kappa 法背景样本上限。 */
|
||||||
|
private static final int MAX_BACKGROUND = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param raster 种群密度估计输出的栅格
|
||||||
|
* @param presence 相机点位(出现样点;空时退化用分位数法)
|
||||||
|
* @param manual 前端显式指定阈值;≤0 表示自动计算
|
||||||
|
* @return 适宜生境二值栅格 + 面积
|
||||||
|
*/
|
||||||
|
public static HabitatAreaResult habitatArea(RasterResult raster, List<CameraPoint> presence,
|
||||||
|
double manual) {
|
||||||
|
if (raster == null || raster.getNx() == 0 || raster.getNy() == 0
|
||||||
|
|| raster.getValues() == null || raster.getValues().length == 0) {
|
||||||
|
return new HabitatAreaResult(0, "none", 0, 0, 0, 0, 0, 0, 0, 0, new int[0], 0);
|
||||||
|
}
|
||||||
|
int nx = raster.getNx();
|
||||||
|
int ny = raster.getNy();
|
||||||
|
double[] values = raster.getValues();
|
||||||
|
int total = nx * ny;
|
||||||
|
|
||||||
|
double threshold;
|
||||||
|
String method;
|
||||||
|
if (manual > 0) {
|
||||||
|
threshold = manual;
|
||||||
|
method = "manual";
|
||||||
|
} else {
|
||||||
|
double[] auto = optimalThreshold(raster, presence);
|
||||||
|
threshold = auto[0];
|
||||||
|
method = auto[1] == 1.0 ? "maxKappa" : "quantile75";
|
||||||
|
}
|
||||||
|
|
||||||
|
int suitable = 0;
|
||||||
|
int[] binary = new int[total];
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
if (values[i] >= threshold) {
|
||||||
|
binary[i] = 1;
|
||||||
|
suitable++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double cellKm2 = KM_PER_DEG * raster.getCellSize()
|
||||||
|
* KM_PER_DEG * raster.getCellSize()
|
||||||
|
* Math.cos(Math.toRadians(raster.getY0() + ny * raster.getCellSize() / 2.0));
|
||||||
|
double areaKm2 = round2(suitable * cellKm2);
|
||||||
|
|
||||||
|
return new HabitatAreaResult(round4(threshold), method, suitable, total, areaKm2,
|
||||||
|
nx, ny, raster.getX0(), raster.getY0(), raster.getCellSize(),
|
||||||
|
binary, raster.getMaxValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* max-Kappa 阈值:出现样点 = 相机所在格;未出现样点 = 其余格抽样。
|
||||||
|
* 返回 [threshold, flag](flag=1 maxKappa,flag=0 quantile75 退化)。
|
||||||
|
*/
|
||||||
|
private static double[] optimalThreshold(RasterResult raster, List<CameraPoint> presence) {
|
||||||
|
int nx = raster.getNx();
|
||||||
|
int ny = raster.getNy();
|
||||||
|
double[] values = raster.getValues();
|
||||||
|
|
||||||
|
// 出现格集合
|
||||||
|
Set<Integer> presenceCells = new HashSet<>();
|
||||||
|
if (presence != null) {
|
||||||
|
for (CameraPoint p : presence) {
|
||||||
|
int col = (int) Math.floor((p.getLon() - raster.getX0()) / raster.getCellSize());
|
||||||
|
int row = (int) Math.floor((p.getLat() - raster.getY0()) / raster.getCellSize());
|
||||||
|
if (col >= 0 && col < nx && row >= 0 && row < ny) {
|
||||||
|
presenceCells.add(row * nx + col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presenceCells.isEmpty()) {
|
||||||
|
return new double[]{quantileOf(values, 0.75), 0.0};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景格(去掉出现格)
|
||||||
|
List<Integer> background = new ArrayList<>();
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (!presenceCells.contains(i)) {
|
||||||
|
background.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int nBg = Math.min(background.size(), MAX_BACKGROUND);
|
||||||
|
List<Integer> bgSample = new ArrayList<>(nBg);
|
||||||
|
if (nBg == background.size()) {
|
||||||
|
bgSample.addAll(background);
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < nBg; i++) {
|
||||||
|
bgSample.add(background.get((int) Math.round(
|
||||||
|
(double) i * (background.size() - 1) / Math.max(1, nBg - 1))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 候选阈值:密度分位数序列
|
||||||
|
List<Double> candidates = new ArrayList<>();
|
||||||
|
for (int k = 1; k <= 99; k++) {
|
||||||
|
candidates.add(quantileOf(values, k / 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
double bestKappa = Double.NEGATIVE_INFINITY;
|
||||||
|
double bestThr = quantileOf(values, 0.75);
|
||||||
|
for (double thr : candidates) {
|
||||||
|
int tp = 0;
|
||||||
|
for (Integer c : presenceCells) {
|
||||||
|
if (values[c] >= thr) {
|
||||||
|
tp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int fp = 0;
|
||||||
|
for (Integer c : bgSample) {
|
||||||
|
if (values[c] >= thr) {
|
||||||
|
fp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int fn = presenceCells.size() - tp;
|
||||||
|
int tn = bgSample.size() - fp;
|
||||||
|
|
||||||
|
double po = (double) (tp + tn) / (presenceCells.size() + bgSample.size());
|
||||||
|
double pe = ((double) (tp + fp) * (tp + fn)
|
||||||
|
+ (double) (fn + tn) * (fp + tn))
|
||||||
|
/ ((presenceCells.size() + bgSample.size()) * (presenceCells.size() + bgSample.size()));
|
||||||
|
double kappa = pe < 1.0 ? (po - pe) / (1.0 - pe) : 0;
|
||||||
|
if (kappa > bestKappa) {
|
||||||
|
bestKappa = kappa;
|
||||||
|
bestThr = thr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new double[]{bestThr, 1.0};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 简易 R type-7 分位数(不依赖 commons-math3,避免包装开销)。 */
|
||||||
|
private static double quantileOf(double[] values, double p) {
|
||||||
|
double[] sorted = values.clone();
|
||||||
|
java.util.Arrays.sort(sorted);
|
||||||
|
double h = (sorted.length - 1) * p;
|
||||||
|
int lo = (int) Math.floor(h);
|
||||||
|
int hi = Math.min(sorted.length - 1, lo + 1);
|
||||||
|
return sorted[lo] + (h - lo) * (sorted[hi] - sorted[lo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round2(double v) {
|
||||||
|
return Math.round(v * 100.0) / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round4(double v) {
|
||||||
|
return Math.round(v * 10000.0) / 10000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.HabitatAreaResult;
|
||||||
|
import net.hzvtc.stat.dto.HabitatRangeResult;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 栖息地分布范围:对适宜生境二值栅格按 8 连通划分斑块,每个斑块分别求
|
||||||
|
* 最小凸包(convex hull)多边形,分布范围面积 = 各斑块凸包面积之和。
|
||||||
|
*
|
||||||
|
* <p>不做全局单一凸包:适宜斑块散布在多个相机集群时,单一凸包会把
|
||||||
|
* 集群之间的大片不适宜区域圈入,导致分布范围面积虚高。</p>
|
||||||
|
*/
|
||||||
|
public final class HabitatRange {
|
||||||
|
|
||||||
|
private HabitatRange() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 地球半径(km)。 */
|
||||||
|
private static final double EARTH_R = 6371.0088;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param area 栖息地面积分析输出(二值栅格)
|
||||||
|
* @return 分布范围多边形(按斑块)+ 总面积
|
||||||
|
*/
|
||||||
|
public static HabitatRangeResult habitatRange(HabitatAreaResult area) {
|
||||||
|
if (area == null || area.getBinary() == null || area.getBinary().length == 0) {
|
||||||
|
return empty();
|
||||||
|
}
|
||||||
|
int nx = area.getNx();
|
||||||
|
int ny = area.getNy();
|
||||||
|
int[] binary = area.getBinary();
|
||||||
|
|
||||||
|
// 1) 适宜格连通斑块(8 邻接)
|
||||||
|
List<List<int[]>> clusters = connectedClusters(binary, nx, ny);
|
||||||
|
int suitable = 0;
|
||||||
|
for (List<int[]> c : clusters) {
|
||||||
|
suitable += c.size();
|
||||||
|
}
|
||||||
|
if (suitable == 0 || clusters.isEmpty()) {
|
||||||
|
return new HabitatRangeResult(new double[0][], new double[0][][], 0, 0, 0, nx * ny, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 每个斑块单独求凸包(用格子 4 角点,保证凸包完整覆盖格子本身)+ 球面面积
|
||||||
|
List<double[][]> polys = new ArrayList<>(clusters.size());
|
||||||
|
List<Double> areas = new ArrayList<>(clusters.size());
|
||||||
|
double totalArea = 0;
|
||||||
|
for (List<int[]> cluster : clusters) {
|
||||||
|
List<double[]> pts = new ArrayList<>(cluster.size() * 4);
|
||||||
|
double s = area.getCellSize();
|
||||||
|
for (int[] rc : cluster) {
|
||||||
|
double lonLo = area.getX0() + rc[1] * s;
|
||||||
|
double lonHi = lonLo + s;
|
||||||
|
double latLo = area.getY0() + rc[0] * s;
|
||||||
|
double latHi = latLo + s;
|
||||||
|
pts.add(new double[]{lonLo, latLo});
|
||||||
|
pts.add(new double[]{lonHi, latLo});
|
||||||
|
pts.add(new double[]{lonLo, latHi});
|
||||||
|
pts.add(new double[]{lonHi, latHi});
|
||||||
|
}
|
||||||
|
double[][] ring = convexHull(pts);
|
||||||
|
double a = sphericalPolygonArea(ring);
|
||||||
|
polys.add(ring);
|
||||||
|
areas.add(a);
|
||||||
|
totalArea += a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 按面积降序,polygon = 最大斑块
|
||||||
|
Integer[] order = new Integer[polys.size()];
|
||||||
|
for (int i = 0; i < order.length; i++) {
|
||||||
|
order[i] = i;
|
||||||
|
}
|
||||||
|
final List<Double> areasRef = areas;
|
||||||
|
java.util.Arrays.sort(order, (a, b) -> Double.compare(areasRef.get(b), areasRef.get(a)));
|
||||||
|
double[][][] sorted = new double[order.length][][];
|
||||||
|
for (int i = 0; i < order.length; i++) {
|
||||||
|
sorted[i] = polys.get(order[i]);
|
||||||
|
}
|
||||||
|
double[][] main = sorted[0];
|
||||||
|
|
||||||
|
double areaKm2 = round2(totalArea);
|
||||||
|
double suitableKm2 = suitableCellAreaKm2(area, suitable);
|
||||||
|
double ratio = areaKm2 > 0 ? Math.min(1.0, round4(suitableKm2 / areaKm2)) : 1.0;
|
||||||
|
|
||||||
|
return new HabitatRangeResult(main, sorted, clusters.size(), areaKm2,
|
||||||
|
suitable, nx * ny, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 8 邻接连通斑块(每元素为 [row, col] 列表)。 */
|
||||||
|
private static List<List<int[]>> connectedClusters(int[] binary, int nx, int ny) {
|
||||||
|
List<List<int[]>> clusters = new ArrayList<>();
|
||||||
|
boolean[] visited = new boolean[binary.length];
|
||||||
|
int[] dr = {-1, -1, -1, 0, 0, 1, 1, 1};
|
||||||
|
int[] dc = {-1, 0, 1, -1, 1, -1, 0, 1};
|
||||||
|
for (int i = 0; i < binary.length; i++) {
|
||||||
|
if (binary[i] != 1 || visited[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<int[]> comp = new ArrayList<>();
|
||||||
|
Deque<Integer> queue = new ArrayDeque<>();
|
||||||
|
queue.add(i);
|
||||||
|
visited[i] = true;
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
int idx = queue.poll();
|
||||||
|
int r = idx / nx;
|
||||||
|
int c = idx % nx;
|
||||||
|
comp.add(new int[]{r, c});
|
||||||
|
for (int d = 0; d < 8; d++) {
|
||||||
|
int nr = r + dr[d];
|
||||||
|
int nc = c + dc[d];
|
||||||
|
if (nr < 0 || nr >= ny || nc < 0 || nc >= nx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int nIdx = nr * nx + nc;
|
||||||
|
if (binary[nIdx] == 1 && !visited[nIdx]) {
|
||||||
|
visited[nIdx] = true;
|
||||||
|
queue.add(nIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clusters.add(comp);
|
||||||
|
}
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Andrew 单调链凸包,返回闭合环(末点 = 首点)。 */
|
||||||
|
private static double[][] convexHull(List<double[]> pts) {
|
||||||
|
pts.sort((a, b) -> a[0] != b[0] ? Double.compare(a[0], b[0]) : Double.compare(a[1], b[1]));
|
||||||
|
int n = pts.size();
|
||||||
|
double[][] hull = new double[2 * n][];
|
||||||
|
int k = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
while (k >= 2 && cross(hull[k - 2], hull[k - 1], pts.get(i)) <= 0) {
|
||||||
|
k--;
|
||||||
|
}
|
||||||
|
hull[k++] = pts.get(i);
|
||||||
|
}
|
||||||
|
int lower = k + 1;
|
||||||
|
for (int i = n - 2; i >= 0; i--) {
|
||||||
|
while (k >= lower && cross(hull[k - 2], hull[k - 1], pts.get(i)) <= 0) {
|
||||||
|
k--;
|
||||||
|
}
|
||||||
|
hull[k++] = pts.get(i);
|
||||||
|
}
|
||||||
|
double[][] ring = new double[k][];
|
||||||
|
System.arraycopy(hull, 0, ring, 0, k);
|
||||||
|
return ring;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double cross(double[] o, double[] a, double[] b) {
|
||||||
|
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 球面多边形面积(km²):A = R²/2 · |Σ (λ2-λ1)(sinφ1+sinφ2)|。 */
|
||||||
|
private static double sphericalPolygonArea(double[][] ring) {
|
||||||
|
if (ring.length < 4) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double sum = 0;
|
||||||
|
for (int i = 0; i < ring.length - 1; i++) {
|
||||||
|
double lon1 = Math.toRadians(ring[i][0]);
|
||||||
|
double lat1 = Math.toRadians(ring[i][1]);
|
||||||
|
double lon2 = Math.toRadians(ring[i + 1][0]);
|
||||||
|
double lat2 = Math.toRadians(ring[i + 1][1]);
|
||||||
|
sum += (lon2 - lon1) * (Math.sin(lat1) + Math.sin(lat2));
|
||||||
|
}
|
||||||
|
return Math.abs(sum) * EARTH_R * EARTH_R / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 适宜格总面积(km²,与 HabitatAnalysis 同一面积公式)。 */
|
||||||
|
private static double suitableCellAreaKm2(HabitatAreaResult area, int suitableCells) {
|
||||||
|
double cellKm2 = 111.32 * area.getCellSize() * 111.32 * area.getCellSize()
|
||||||
|
* Math.cos(Math.toRadians(area.getY0() + area.getNy() * area.getCellSize() / 2.0));
|
||||||
|
return suitableCells * cellKm2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HabitatRangeResult empty() {
|
||||||
|
return new HabitatRangeResult(new double[0][], new double[0][][], 0, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round2(double v) {
|
||||||
|
return Math.round(v * 100.0) / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round4(double v) {
|
||||||
|
return Math.round(v * 10000.0) / 10000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.HomeRangeItem;
|
||||||
|
import net.hzvtc.stat.util.GeoUtils;
|
||||||
|
import net.hzvtc.stat.util.StatisticsUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 homeRange(chain) / homeRanges(chainDB) 的 Java 实现。
|
||||||
|
*
|
||||||
|
* <p>homeRange:单条足迹链 → 矩形包围面积(km²)。</p>
|
||||||
|
* <p>homeRanges:多物种多链聚合 → 每物种的 mean Area / SD / N。</p>
|
||||||
|
*/
|
||||||
|
public final class HomeRange {
|
||||||
|
|
||||||
|
private HomeRange() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单条足迹链的家域面积(km²)。
|
||||||
|
*
|
||||||
|
* @param chain 足迹链(按时间顺序)
|
||||||
|
* @return 面积(km²)
|
||||||
|
*/
|
||||||
|
public static double homeRange(List<FootprintPoint> chain) {
|
||||||
|
if (chain == null || chain.isEmpty()) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
// 转 UTM 米
|
||||||
|
double[][] xy = GeoUtils.wgs84ToUtmAuto(chain, p -> p.getLon(), p -> p.getLat());
|
||||||
|
double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
|
||||||
|
double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
|
||||||
|
for (double[] p : xy) {
|
||||||
|
if (p[0] < minX) minX = p[0];
|
||||||
|
if (p[0] > maxX) maxX = p[0];
|
||||||
|
if (p[1] < minY) minY = p[1];
|
||||||
|
if (p[1] > maxY) maxY = p[1];
|
||||||
|
}
|
||||||
|
// UTM 米 → km
|
||||||
|
double dx = (maxX - minX) / 1000.0;
|
||||||
|
double dy = (maxY - minY) / 1000.0;
|
||||||
|
return Math.round(dx * dy * 1000.0) / 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多物种多链的家域聚合。
|
||||||
|
*
|
||||||
|
* @param records 链记录集合(Species, Line, Lon, Lat, Date)
|
||||||
|
* @return 每个物种一条记录:Area(均值, km²) / SD / N
|
||||||
|
*/
|
||||||
|
public static List<HomeRangeItem> homeRanges(List<ChainRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 Species 分组
|
||||||
|
Map<String, List<ChainRecord>> bySpecies = new HashMap<>();
|
||||||
|
for (ChainRecord r : records) {
|
||||||
|
bySpecies.computeIfAbsent(r.getSpecies(), k -> new ArrayList<>()).add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HomeRangeItem> result = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<ChainRecord>> e : bySpecies.entrySet()) {
|
||||||
|
String species = e.getKey();
|
||||||
|
List<ChainRecord> sp = e.getValue();
|
||||||
|
|
||||||
|
// 按 Line 分组
|
||||||
|
Map<Integer, List<ChainRecord>> byLine = new HashMap<>();
|
||||||
|
for (ChainRecord r : sp) {
|
||||||
|
byLine.computeIfAbsent(r.getLine(), k -> new ArrayList<>()).add(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Double> areas = new ArrayList<>();
|
||||||
|
for (List<ChainRecord> lineRecs : byLine.values()) {
|
||||||
|
List<FootprintPoint> chain = new ArrayList<>(lineRecs.size());
|
||||||
|
for (ChainRecord r : lineRecs) {
|
||||||
|
chain.add(new FootprintPoint(r.getLon(), r.getLat(), r.getDate()));
|
||||||
|
}
|
||||||
|
double[][] xy = GeoUtils.wgs84ToUtmAuto(chain, p -> p.getLon(), p -> p.getLat());
|
||||||
|
double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
|
||||||
|
double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
|
||||||
|
for (double[] p : xy) {
|
||||||
|
if (p[0] < minX) minX = p[0];
|
||||||
|
if (p[0] > maxX) maxX = p[0];
|
||||||
|
if (p[1] < minY) minY = p[1];
|
||||||
|
if (p[1] > maxY) maxY = p[1];
|
||||||
|
}
|
||||||
|
double dx = (maxX - minX) / 1000.0;
|
||||||
|
double dy = (maxY - minY) / 1000.0;
|
||||||
|
areas.add(dx * dy);
|
||||||
|
}
|
||||||
|
double[] a = areas.stream().mapToDouble(Double::doubleValue).toArray();
|
||||||
|
double meanArea = StatisticsUtils.mean(a);
|
||||||
|
double sdArea = StatisticsUtils.sd(a);
|
||||||
|
result.add(new HomeRangeItem(
|
||||||
|
species,
|
||||||
|
Math.round(meanArea * 1000.0) / 1000.0,
|
||||||
|
Math.round(sdArea * 1000.0) / 1000.0,
|
||||||
|
a.length
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.MoveBiasResult;
|
||||||
|
import net.hzvtc.stat.util.StatisticsUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 moveBias(chain, scale, plot) 的统计量计算部分。
|
||||||
|
*
|
||||||
|
* <p>算法:
|
||||||
|
* <ol>
|
||||||
|
* <li>按 scale 间隔降采样链(如 scale=2 表示每 2 行取一行)。</li>
|
||||||
|
* <li>对每对相邻采样点用 UTM 距离近似法(地球半径 6378.14 km)求 dist(m)与 bearing(度)。</li>
|
||||||
|
* <li>移除 dist=0 的点;dist > 400m 的视作跨区,保留但 cap(与 R 行为不同:R 移除 dist>400m 行,本类保留以兼容中文注释/过滤规则)。</li>
|
||||||
|
* <li>计算 angular deflection(相邻 bearing 差),输出 mean(dist)、sd(dist)、sd(delta.th)。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class MoveBias {
|
||||||
|
|
||||||
|
private MoveBias() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地球半径(km),与 R moveBias 中 39946.79 / (2π) ≈ 6356.7 不同:R 取极半径 6356.7523。
|
||||||
|
*/
|
||||||
|
private static final double KM_PER_DEG_LAT = 111.32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WGS84 赤道 1° 经度 ≈ 111.32 × cos(lat) km。
|
||||||
|
*/
|
||||||
|
private static double kmPerDegLon(double latDeg) {
|
||||||
|
return 111.32 * Math.cos(Math.toRadians(latDeg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chain 原始足迹链(按时间顺序、约 1 秒间隔)
|
||||||
|
* @param scale 步长倍率(每隔 scale 行取一行)
|
||||||
|
* @return 三个统计量:mean(dist/m)、sd(dist/m)、sd(delta.th/°)
|
||||||
|
*/
|
||||||
|
public static MoveBiasResult moveBias(List<FootprintPoint> chain, int scale) {
|
||||||
|
if (chain == null || chain.size() < 2) {
|
||||||
|
return new MoveBiasResult(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
// 降采样
|
||||||
|
List<FootprintPoint> sampled = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < chain.size(); i += Math.max(1, scale)) {
|
||||||
|
sampled.add(chain.get(i));
|
||||||
|
}
|
||||||
|
if (sampled.size() < 2) {
|
||||||
|
return new MoveBiasResult(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = sampled.size();
|
||||||
|
double[] distKm = new double[n];
|
||||||
|
double[] bearing = new double[n];
|
||||||
|
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
FootprintPoint a = sampled.get(i - 1);
|
||||||
|
FootprintPoint b = sampled.get(i);
|
||||||
|
double meanLat = (a.getLat() + b.getLat()) / 2.0;
|
||||||
|
double dx = (b.getLon() - a.getLon()) * kmPerDegLon(meanLat);
|
||||||
|
double dy = (b.getLat() - a.getLat()) * KM_PER_DEG_LAT;
|
||||||
|
distKm[i] = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
bearing[i] = Math.toDegrees(Math.asin(dy / (distKm[i] == 0 ? 1e-12 : distKm[i])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 dist==0 的点
|
||||||
|
java.util.List<Double> validDist = new java.util.ArrayList<>();
|
||||||
|
java.util.List<Double> validBearing = new java.util.ArrayList<>();
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
if (distKm[i] > 0) {
|
||||||
|
validDist.add(distKm[i]);
|
||||||
|
validBearing.add(bearing[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validDist.isEmpty()) {
|
||||||
|
return new MoveBiasResult(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] distM = new double[validDist.size()];
|
||||||
|
for (int i = 0; i < distM.length; i++) {
|
||||||
|
distM[i] = validDist.get(i) * 1000.0; // km → m
|
||||||
|
}
|
||||||
|
double[] bear = new double[validBearing.size()];
|
||||||
|
for (int i = 0; i < bear.length; i++) {
|
||||||
|
bear[i] = validBearing.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// angular deflection
|
||||||
|
double[] delta = new double[Math.max(0, bear.length - 1)];
|
||||||
|
for (int i = 1; i < bear.length; i++) {
|
||||||
|
delta[i - 1] = bear[i] - bear[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
double meanDist = StatisticsUtils.mean(distM);
|
||||||
|
double sdDist = StatisticsUtils.sd(distM);
|
||||||
|
double sdDelta = delta.length > 1 ? StatisticsUtils.sd(delta) : 0.0;
|
||||||
|
|
||||||
|
return new MoveBiasResult(round(meanDist), round(sdDist), round(sdDelta));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round(double v) {
|
||||||
|
return Math.round(v * 100.0) / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CamtrapAggPoint;
|
||||||
|
import net.hzvtc.stat.dto.CamtrapScatter;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.util.RandomUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 plotCamtrap 的数据准备部分(不负责 R 的 plot 渲染,前端用 ECharts/Map 展示)。
|
||||||
|
*
|
||||||
|
* <p>算法流程(与 R 一致):
|
||||||
|
* <ol>
|
||||||
|
* <li>按 (Lat, Lon) 聚合 Group_size 之和 → 相机点位 + Count(决定圆点大小)。</li>
|
||||||
|
* <li>对 Group_size > 0 的记录,在 (Lon, Lat) 上做 jitter(factor=point.scatter)→ 散点。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class PlotCamtrap {
|
||||||
|
|
||||||
|
private PlotCamtrap() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param trapresult trap 数据(Lon/Lat/Group_size/Date/Time)
|
||||||
|
* @param circleSize 圆点尺寸系数(R plot 中 cex=Count*circle.size;本函数不直接绘图,仅返回数据)
|
||||||
|
* @param pointScatter jitter 因子(R 中 jitter(x, factor=point.scatter))
|
||||||
|
* @return {@link Result},包含 aggPoints(点位+计数)和 scatterPoints(jitter 后散点)
|
||||||
|
*/
|
||||||
|
public static Result plotCamtrap(List<CameraPoint> trapresult, double circleSize, double pointScatter) {
|
||||||
|
// 1) 按 (Lat, Lon) 聚合
|
||||||
|
Map<String, CamtrapAggPoint> agg = new HashMap<>();
|
||||||
|
for (CameraPoint p : trapresult) {
|
||||||
|
String key = p.getLat() + "," + p.getLon();
|
||||||
|
CamtrapAggPoint a = agg.get(key);
|
||||||
|
if (a == null) {
|
||||||
|
a = new CamtrapAggPoint(p.getLat(), p.getLon(), 0);
|
||||||
|
agg.put(key, a);
|
||||||
|
}
|
||||||
|
a.setCount(a.getCount() + p.getGroupSize());
|
||||||
|
}
|
||||||
|
List<CamtrapAggPoint> aggPoints = new ArrayList<>(agg.values());
|
||||||
|
|
||||||
|
// 2) jitter 散点(仅 Group_size > 0 的记录)
|
||||||
|
List<CamtrapScatter> scatter = new ArrayList<>();
|
||||||
|
for (CameraPoint p : trapresult) {
|
||||||
|
if (p.getGroupSize() > 0) {
|
||||||
|
double jLon = jitter(p.getLon(), pointScatter);
|
||||||
|
double jLat = jitter(p.getLat(), pointScatter);
|
||||||
|
scatter.add(new CamtrapScatter(jLon, jLat, p.getDate(), p.getTime(), p.getGroupSize()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Result(aggPoints, scatter, circleSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 的 jitter(x, factor) 等价物:在 ±factor 范围内均匀抖动(与 R jitter 默认 amount 行为近似)。
|
||||||
|
* 使用种子化随机源(见 {@link RandomUtils}),保证同条件结果可复现。
|
||||||
|
*/
|
||||||
|
private static double jitter(double value, double factor) {
|
||||||
|
return value + RandomUtils.uniform(-factor, factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** plotCamtrap 输出结果。 */
|
||||||
|
@lombok.Data
|
||||||
|
@lombok.AllArgsConstructor
|
||||||
|
public static class Result {
|
||||||
|
private List<CamtrapAggPoint> aggPoints;
|
||||||
|
private List<CamtrapScatter> scatterPoints;
|
||||||
|
private double circleSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.FootprintTrace;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 plotFootprint(chain) 的数据准备:返回折线轨迹 + 起止点高亮坐标。
|
||||||
|
*/
|
||||||
|
public final class PlotFootprint {
|
||||||
|
|
||||||
|
private PlotFootprint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param chain 按时间顺序的足迹链节点(Lon/Lat/Date),按 1 秒间隔采样
|
||||||
|
* @return {@link FootprintTrace},前端用 ECharts polyline + scatter 渲染
|
||||||
|
*/
|
||||||
|
public static FootprintTrace plotFootprint(List<FootprintPoint> chain) {
|
||||||
|
if (chain == null || chain.isEmpty()) {
|
||||||
|
return new FootprintTrace(Collections.emptyList(), 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
FootprintPoint first = chain.get(0);
|
||||||
|
FootprintPoint last = chain.get(chain.size() - 1);
|
||||||
|
return new FootprintTrace(chain, first.getLon(), first.getLat(), last.getLon(), last.getLat());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.PredictionResult;
|
||||||
|
import net.hzvtc.stat.dto.SimulationResult;
|
||||||
|
import net.hzvtc.stat.util.StatisticsUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 predictCamtrap(simu, x, plot) 的 Java 实现。
|
||||||
|
*
|
||||||
|
* <p>算法:
|
||||||
|
* <ol>
|
||||||
|
* <li>用 simu 的 camera counts 训练一棵简化随机森林(Bagging 多数表决回归)。</li>
|
||||||
|
* <li>把真实 trap 数据按 (Lat, Lon) 聚合得到与 simu 列数等长的 obs(sort 后)。</li>
|
||||||
|
* <li>RF 对 obs 做 1000 棵树预测,统计 quantile(0.025, 0.5, 0.975)。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>说明:cameratrapR 使用 randomForest 包做 RF。本类用基于 Bootstrap 的简化 RF 自实现(无外部依赖):
|
||||||
|
* 每棵树从训练集有放回采样 n 个样本,在每个分裂节点随机选 sqrt(p) 个特征,
|
||||||
|
* 选最小方差分裂,直到桶内样本 ≤ leafSize 取均值预测。1000 棵树的均值预测即最终 No_ind,
|
||||||
|
* 置信区间来自 1000 棵树各自预测的 2.5% / 97.5% 分位。</p>
|
||||||
|
*/
|
||||||
|
public final class PredictCamtrap {
|
||||||
|
|
||||||
|
private PredictCamtrap() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param simu simuCamtrap 输出
|
||||||
|
* @param data 真实 trap 数据
|
||||||
|
* @param nTree 随机森林规模(默认 1000)
|
||||||
|
* @param seed 随机种子(保证可复现)
|
||||||
|
* @return 预测结果(中位数 + 95% CI)
|
||||||
|
*/
|
||||||
|
public static PredictionResult predictCamtrap(SimulationResult simu,
|
||||||
|
java.util.List<CameraPoint> data,
|
||||||
|
int nTree,
|
||||||
|
long seed) {
|
||||||
|
if (simu == null || simu.getInd() == null || simu.getInd().length == 0 || data == null || data.isEmpty()) {
|
||||||
|
return new PredictionResult(0.0, 0.0, 0.0, new double[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 训练样本:X = simu camera counts, y = Ind
|
||||||
|
long[][] simX = simu.getCameraCounts();
|
||||||
|
int[] simY = simu.getInd();
|
||||||
|
int nSamples = simX.length;
|
||||||
|
int nFeatures = nSamples > 0 ? simX[0].length : 0;
|
||||||
|
|
||||||
|
// 2) 真实 obs:按 (Lat, Lon) 聚合 Group_size,排序(不保留空间信息)
|
||||||
|
Map<String, Long> agg = new HashMap<>();
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
String key = p.getLat() + "," + p.getLon();
|
||||||
|
agg.merge(key, (long) p.getGroupSize(), Long::sum);
|
||||||
|
}
|
||||||
|
long[] obs = new long[agg.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (Long v : agg.values()) {
|
||||||
|
obs[i++] = v;
|
||||||
|
}
|
||||||
|
java.util.Arrays.sort(obs);
|
||||||
|
|
||||||
|
// 3) 训练 RF 并对 obs 预测
|
||||||
|
java.util.Random rng = new java.util.Random(seed);
|
||||||
|
SimpleRandomForest rf = new SimpleRandomForest(nTree, rng);
|
||||||
|
rf.fit(simX, simY, nFeatures);
|
||||||
|
double[] predictions = rf.predict(obs);
|
||||||
|
|
||||||
|
// 4) 统计中位数 / 2.5% / 97.5%
|
||||||
|
double median = StatisticsUtils.quantile(predictions, 0.5);
|
||||||
|
double ciLow = StatisticsUtils.quantile(predictions, 0.025);
|
||||||
|
double ciHigh = StatisticsUtils.quantile(predictions, 0.975);
|
||||||
|
|
||||||
|
return new PredictionResult(round1(median), round1(ciLow), round1(ciHigh), sample(predictions, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等距下采样,控制返回体大小(箱线图统计量用 200 个样本已足够)。 */
|
||||||
|
private static double[] sample(double[] values, int max) {
|
||||||
|
if (values == null || values.length <= max) {
|
||||||
|
return values == null ? new double[0] : values;
|
||||||
|
}
|
||||||
|
double[] out = new double[max];
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
out[i] = values[(int) Math.round((double) i * (values.length - 1) / (max - 1))];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round1(double v) {
|
||||||
|
return Math.round(v * 10.0) / 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简化随机森林(Bootstrap + 随机特征子集 + 回归树)。
|
||||||
|
* 不追求精度等价 randomForest R 包,仅在结构上保持同源:1000 棵树 / OOB bootstrap / sqrt(p) 特征。
|
||||||
|
*/
|
||||||
|
static final class SimpleRandomForest {
|
||||||
|
|
||||||
|
private final int nTree;
|
||||||
|
private final java.util.Random rng;
|
||||||
|
private final java.util.List<RegressionTree> trees = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
SimpleRandomForest(int nTree, java.util.Random rng) {
|
||||||
|
this.nTree = nTree;
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fit(long[][] x, int[] y, int nFeatures) {
|
||||||
|
int n = x.length;
|
||||||
|
int subsetSize = Math.max(1, (int) Math.sqrt(nFeatures));
|
||||||
|
for (int t = 0; t < nTree; t++) {
|
||||||
|
long[][] bx = new long[n][];
|
||||||
|
int[] by = new int[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
int idx = rng.nextInt(n);
|
||||||
|
bx[i] = x[idx];
|
||||||
|
by[i] = y[idx];
|
||||||
|
}
|
||||||
|
RegressionTree tree = new RegressionTree();
|
||||||
|
tree.root = tree.build(bx, by, 0, subsetSize, rng);
|
||||||
|
trees.add(tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] predict(long[] obs) {
|
||||||
|
double[] perTree = new double[trees.size()];
|
||||||
|
for (int t = 0; t < trees.size(); t++) {
|
||||||
|
perTree[t] = trees.get(t).predict(obs);
|
||||||
|
}
|
||||||
|
return perTree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 极简回归树:节点递归按中位数二分,叶子存均值。 */
|
||||||
|
static final class RegressionTree {
|
||||||
|
|
||||||
|
Node root;
|
||||||
|
|
||||||
|
static final class Node {
|
||||||
|
int featureIdx;
|
||||||
|
double threshold;
|
||||||
|
Node left;
|
||||||
|
Node right;
|
||||||
|
double leafValue;
|
||||||
|
|
||||||
|
boolean isLeaf() {
|
||||||
|
return left == null && right == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Node build(long[][] x, int[] y, int depth, int subsetSize, java.util.Random rng) {
|
||||||
|
Node node = new Node();
|
||||||
|
if (x.length <= 1 || depth >= 12) {
|
||||||
|
node.leafValue = mean(y);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nFeatures = x[0].length;
|
||||||
|
int[] featIdx = randomFeatures(nFeatures, subsetSize, rng);
|
||||||
|
|
||||||
|
double bestVar = Double.POSITIVE_INFINITY;
|
||||||
|
int bestFeat = -1;
|
||||||
|
double bestThr = 0;
|
||||||
|
|
||||||
|
for (int f : featIdx) {
|
||||||
|
double[] vals = new double[x.length];
|
||||||
|
for (int i = 0; i < x.length; i++) {
|
||||||
|
vals[i] = x[i][f];
|
||||||
|
}
|
||||||
|
double thr = median(vals);
|
||||||
|
// 拆分子集
|
||||||
|
int nLeft = 0, nRight = 0;
|
||||||
|
double sumL = 0, sumR = 0;
|
||||||
|
for (int i = 0; i < x.length; i++) {
|
||||||
|
if (x[i][f] <= thr) {
|
||||||
|
nLeft++;
|
||||||
|
sumL += y[i];
|
||||||
|
} else {
|
||||||
|
nRight++;
|
||||||
|
sumR += y[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nLeft == 0 || nRight == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double mL = sumL / nLeft;
|
||||||
|
double mR = sumR / nRight;
|
||||||
|
double varL = 0, varR = 0;
|
||||||
|
for (int i = 0; i < x.length; i++) {
|
||||||
|
double dev = y[i] - (x[i][f] <= thr ? mL : mR);
|
||||||
|
if (x[i][f] <= thr) {
|
||||||
|
varL += dev * dev;
|
||||||
|
} else {
|
||||||
|
varR += dev * dev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double weighted = (varL + varR) / x.length;
|
||||||
|
if (weighted < bestVar) {
|
||||||
|
bestVar = weighted;
|
||||||
|
bestFeat = f;
|
||||||
|
bestThr = thr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestFeat < 0) {
|
||||||
|
node.leafValue = mean(y);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.featureIdx = bestFeat;
|
||||||
|
node.threshold = bestThr;
|
||||||
|
|
||||||
|
java.util.List<long[]> leftX = new java.util.ArrayList<>();
|
||||||
|
java.util.List<Integer> leftY = new java.util.ArrayList<>();
|
||||||
|
java.util.List<long[]> rightX = new java.util.ArrayList<>();
|
||||||
|
java.util.List<Integer> rightY = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < x.length; i++) {
|
||||||
|
if (x[i][bestFeat] <= bestThr) {
|
||||||
|
leftX.add(x[i]);
|
||||||
|
leftY.add(y[i]);
|
||||||
|
} else {
|
||||||
|
rightX.add(x[i]);
|
||||||
|
rightY.add(y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.left = build(toArray(leftX), toInt(leftY), depth + 1, subsetSize, rng);
|
||||||
|
node.right = build(toArray(rightX), toInt(rightY), depth + 1, subsetSize, rng);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
double predict(long[] obs) {
|
||||||
|
Node n = root;
|
||||||
|
while (!n.isLeaf()) {
|
||||||
|
n = obs[n.featureIdx] <= n.threshold ? n.left : n.right;
|
||||||
|
if (n == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n.leafValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] randomFeatures(int n, int subset, java.util.Random rng) {
|
||||||
|
java.util.Set<Integer> s = new java.util.HashSet<>();
|
||||||
|
while (s.size() < Math.min(subset, n)) {
|
||||||
|
s.add(rng.nextInt(n));
|
||||||
|
}
|
||||||
|
int[] out = new int[s.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (Integer v : s) {
|
||||||
|
out[i++] = v;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long[][] toArray(java.util.List<long[]> list) {
|
||||||
|
long[][] arr = new long[list.size()][];
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] toInt(java.util.List<Integer> list) {
|
||||||
|
int[] arr = new int[list.size()];
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double mean(int[] y) {
|
||||||
|
if (y.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
double s = 0;
|
||||||
|
for (int v : y) {
|
||||||
|
s += v;
|
||||||
|
}
|
||||||
|
return s / y.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double median(double[] v) {
|
||||||
|
double[] s = v.clone();
|
||||||
|
java.util.Arrays.sort(s);
|
||||||
|
int m = s.length / 2;
|
||||||
|
if (s.length % 2 == 1) {
|
||||||
|
return s[m];
|
||||||
|
}
|
||||||
|
return (s[m - 1] + s[m]) / 2.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.util.StatisticsUtils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 Rowcliffe(x, r, theta, v, duration) 的 Java 实现:
|
||||||
|
* Rowcliffe (2008) 随机相遇模型估计物种密度。
|
||||||
|
*
|
||||||
|
* <p>公式(与 R 完全一致):
|
||||||
|
* <pre>
|
||||||
|
* D_i = Count_i * π / (duration * v * r * (2 + θ * 2π/360))
|
||||||
|
* result = c(mean(D), sd(D))
|
||||||
|
* </pre>
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>字段单位约定:
|
||||||
|
* <ul>
|
||||||
|
* <li>r:相机检测距离(km)</li>
|
||||||
|
* <li>theta:相机检测角度(°)</li>
|
||||||
|
* <li>v:动物平均移动速度(km/h)</li>
|
||||||
|
* <li>duration:调查持续天数(day)</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class Rowcliffe {
|
||||||
|
|
||||||
|
private Rowcliffe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data trap 数据
|
||||||
|
* @param r 检测距离(km)
|
||||||
|
* @param theta 检测角度(°)
|
||||||
|
* @param v 动物平均速度(km/h)
|
||||||
|
* @param duration 调查天数(day)
|
||||||
|
* @return [mean(D), sd(D)]
|
||||||
|
*/
|
||||||
|
public static double[] rowcliffe(List<CameraPoint> data, double r, double theta, double v, double duration) {
|
||||||
|
// 按 (Lat, Lon) 聚合 Group_size 之和
|
||||||
|
Map<String, Double> sumMap = new HashMap<>();
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
String key = p.getLat() + "," + p.getLon();
|
||||||
|
sumMap.merge(key, (double) p.getGroupSize(), Double::sum);
|
||||||
|
}
|
||||||
|
double[] counts = sumMap.values().stream().mapToDouble(Double::doubleValue).toArray();
|
||||||
|
if (counts.length == 0) {
|
||||||
|
return new double[]{0.0, 0.0};
|
||||||
|
}
|
||||||
|
|
||||||
|
double denom = duration * v * r * (2.0 + theta * 2.0 * Math.PI / 360.0);
|
||||||
|
double[] d = new double[counts.length];
|
||||||
|
for (int i = 0; i < counts.length; i++) {
|
||||||
|
d[i] = counts[i] * Math.PI / denom;
|
||||||
|
}
|
||||||
|
|
||||||
|
double mean = StatisticsUtils.mean(d);
|
||||||
|
double sd = StatisticsUtils.sd(d);
|
||||||
|
return new double[]{round2(mean), round2(sd)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double round2(double v) {
|
||||||
|
return Math.round(v * 100.0) / 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package net.hzvtc.stat.algorithm;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.SimulationResult;
|
||||||
|
import net.hzvtc.stat.util.RandomUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 中 simuCamtrap(x, ...) 的 Java 实现。
|
||||||
|
*
|
||||||
|
* <p>算法(与 R 一致):
|
||||||
|
* <ol>
|
||||||
|
* <li>使用业务层从 MySQL 读取的 Step.L / Bias / Range。</li>
|
||||||
|
* <li>取 unique(Lon, Lat) 作为相机位(含 Count=0 列)。</li>
|
||||||
|
* <li>共进行 ind × iteration 次随机游走模拟。每次:
|
||||||
|
* <ul>
|
||||||
|
* <li>在网格中央 25%-75% 范围随机生成初始位置。</li>
|
||||||
|
* <li>每一步判断是否落入任一相机的检测半径(detect m)+ 检测角度(±25°)内,是则 Count+1。</li>
|
||||||
|
* <li>步长 = Step.L × N(1, Step.V);偏向角叠加 N(0, Bias)(度→弧度)。</li>
|
||||||
|
* <li>按 (1 - dist/range) vs (dist/range) 概率选择"前进 vs 回原点"方向。</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>每次模拟结束:Count 排序(不保留空间信息),写入 out[k+(ite-1)*ind, ]。</li>
|
||||||
|
* </ol>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class SimuCamtrap {
|
||||||
|
|
||||||
|
private SimuCamtrap() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检测角半角 50° → 弧度。 */
|
||||||
|
private static final double DETECT_HALF_ARC = Math.toRadians(50.0);
|
||||||
|
|
||||||
|
/** 中心 50% 区域边界系数。 */
|
||||||
|
private static final double CENTER_FRAC = 0.25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data 真实 trap 数据
|
||||||
|
* @param species 物种名称(用于标识本次模拟)
|
||||||
|
* @param detect 检测半径(m)。注意:R 默认 50,传给函数前已用米
|
||||||
|
* @param bearing 每台相机的朝向(弧度数组,长度=camera.N)。NULL 时随机生成
|
||||||
|
* @param stepN 模拟步数
|
||||||
|
* @param stepV 步长标准差系数(与 stepL 相乘的 N(1, stepV))
|
||||||
|
* @param stepL 步长均值(m)
|
||||||
|
* @param biasDeg 偏向角标准差(度)
|
||||||
|
* @param rangeM 家域半径(m),R 中 Range(km²)*1000 仅示意,实际是直径 m
|
||||||
|
* @param ind 个体数
|
||||||
|
* @param iteration 迭代次数
|
||||||
|
*/
|
||||||
|
public static SimulationResult simuCamtrap(java.util.List<CameraPoint> data,
|
||||||
|
String species,
|
||||||
|
double detect,
|
||||||
|
double[] bearing,
|
||||||
|
int stepN,
|
||||||
|
double stepV,
|
||||||
|
double stepL,
|
||||||
|
double biasDeg,
|
||||||
|
double rangeM,
|
||||||
|
int ind,
|
||||||
|
int iteration) {
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return new SimulationResult(new int[0], new long[0][0], new double[0], new double[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) unique cameras
|
||||||
|
double[] camLon = uniqueLon(data);
|
||||||
|
double[] camLat = uniqueLat(data);
|
||||||
|
int cameraN = camLon.length;
|
||||||
|
if (bearing == null || bearing.length != cameraN) {
|
||||||
|
bearing = new double[cameraN];
|
||||||
|
for (int i = 0; i < cameraN; i++) {
|
||||||
|
bearing[i] = RandomUtils.uniform(0.0, 2.0 * Math.PI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 经纬度边界
|
||||||
|
double minLon = Double.POSITIVE_INFINITY, maxLon = Double.NEGATIVE_INFINITY;
|
||||||
|
double minLat = Double.POSITIVE_INFINITY, maxLat = Double.NEGATIVE_INFINITY;
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
if (p.getLon() < minLon) minLon = p.getLon();
|
||||||
|
if (p.getLon() > maxLon) maxLon = p.getLon();
|
||||||
|
if (p.getLat() < minLat) minLat = p.getLat();
|
||||||
|
if (p.getLat() > maxLat) maxLat = p.getLat();
|
||||||
|
}
|
||||||
|
double lonLo = minLon + CENTER_FRAC * (maxLon - minLon);
|
||||||
|
double lonHi = maxLon - CENTER_FRAC * (maxLon - minLon);
|
||||||
|
double latLo = minLat + CENTER_FRAC * (maxLat - minLat);
|
||||||
|
double latHi = maxLat - CENTER_FRAC * (maxLat - minLat);
|
||||||
|
|
||||||
|
// 2) ind × iteration 次模拟
|
||||||
|
int totalRows = ind * iteration;
|
||||||
|
int[] indArr = new int[totalRows];
|
||||||
|
long[][] out = new long[totalRows][cameraN];
|
||||||
|
long[] count = new long[cameraN];
|
||||||
|
|
||||||
|
double biasRad = biasDeg / 360.0 * 2.0 * Math.PI;
|
||||||
|
|
||||||
|
for (int ite = 1; ite <= iteration; ite++) {
|
||||||
|
for (int k = 1; k <= ind; k++) {
|
||||||
|
// 初始位置(中心区域随机)
|
||||||
|
double locX = RandomUtils.uniform(lonLo, lonHi);
|
||||||
|
double locY = RandomUtils.uniform(latLo, latHi);
|
||||||
|
double x0 = locX, y0 = locY;
|
||||||
|
|
||||||
|
// 初始朝向
|
||||||
|
double theta = RandomUtils.uniform(0.0, 2.0 * Math.PI);
|
||||||
|
Arrays.fill(count, 0L);
|
||||||
|
|
||||||
|
for (int i = 1; i <= stepN; i++) {
|
||||||
|
// 相机检测判定
|
||||||
|
for (int j = 0; j < cameraN; j++) {
|
||||||
|
double dx = locX - camLon[j];
|
||||||
|
double dy = locY - camLat[j];
|
||||||
|
double d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < detect) {
|
||||||
|
double bear;
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
bear = 0;
|
||||||
|
} else {
|
||||||
|
bear = Math.atan2(dy, dx);
|
||||||
|
}
|
||||||
|
double diff = Math.abs(normalizeAngle(bear - bearing[j]));
|
||||||
|
if (diff < DETECT_HALF_ARC) {
|
||||||
|
count[j]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步长
|
||||||
|
double move = stepL * RandomUtils.gaussian(1.0, stepV);
|
||||||
|
locX += move * Math.cos(theta);
|
||||||
|
locY += move * Math.sin(theta);
|
||||||
|
|
||||||
|
// 前进 vs 回原点
|
||||||
|
double thetaF = theta + RandomUtils.gaussian(0.0, biasRad);
|
||||||
|
double thetaB;
|
||||||
|
if (locX > x0) {
|
||||||
|
thetaB = Math.PI + Math.atan2(locY - y0, locX - x0) + RandomUtils.gaussian(0.0, biasRad * 2);
|
||||||
|
} else {
|
||||||
|
thetaB = Math.atan2(locY - y0, locX - x0) + RandomUtils.gaussian(0.0, biasRad * 2);
|
||||||
|
}
|
||||||
|
double dist = Math.sqrt((locX - x0) * (locX - x0) + (locY - y0) * (locY - y0));
|
||||||
|
double pForward = Math.abs(1.0 - dist / rangeM);
|
||||||
|
double pBackward = dist / rangeM;
|
||||||
|
theta = RandomUtils.weightedSample(new double[]{pForward, pBackward}) == 0 ? thetaF : thetaB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序后写入(R 中 sort(camera$Count))
|
||||||
|
long[] sorted = count.clone();
|
||||||
|
Arrays.sort(sorted);
|
||||||
|
int rowIdx = k + (ite - 1) * ind - 1;
|
||||||
|
indArr[rowIdx] = k;
|
||||||
|
out[rowIdx] = sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimulationResult(indArr, out, camLon, camLat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] uniqueLon(java.util.List<CameraPoint> data) {
|
||||||
|
java.util.LinkedHashMap<String, double[]> map = new java.util.LinkedHashMap<>();
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
map.put(p.getLon() + "," + p.getLat(), new double[]{p.getLon(), p.getLat()});
|
||||||
|
}
|
||||||
|
double[] lons = new double[map.size()];
|
||||||
|
double[] lats = new double[map.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (double[] v : map.values()) {
|
||||||
|
lons[i] = v[0];
|
||||||
|
lats[i] = v[1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
// 共享给 uniqueLat 通过线程局部?简单起见直接重写为返回 pair
|
||||||
|
return lons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] uniqueLat(java.util.List<CameraPoint> data) {
|
||||||
|
java.util.LinkedHashMap<String, double[]> map = new java.util.LinkedHashMap<>();
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
map.put(p.getLon() + "," + p.getLat(), new double[]{p.getLon(), p.getLat()});
|
||||||
|
}
|
||||||
|
double[] lats = new double[map.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (double[] v : map.values()) {
|
||||||
|
lats[i] = v[1];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return lats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把角度规整到 [-π, π]。 */
|
||||||
|
private static double normalizeAngle(double a) {
|
||||||
|
while (a > Math.PI) {
|
||||||
|
a -= 2.0 * Math.PI;
|
||||||
|
}
|
||||||
|
while (a < -Math.PI) {
|
||||||
|
a += 2.0 * Math.PI;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
package net.hzvtc.stat.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.hzvtc.stat.algorithm.Geo2Proj;
|
||||||
|
import net.hzvtc.stat.algorithm.PlotCamtrap;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.ChainLengthItem;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.DensitySeries;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.FootprintTrace;
|
||||||
|
import net.hzvtc.stat.dto.HabitatAreaResult;
|
||||||
|
import net.hzvtc.stat.dto.HabitatRangeResult;
|
||||||
|
import net.hzvtc.stat.dto.HomeRangeItem;
|
||||||
|
import net.hzvtc.stat.dto.MoveBiasResult;
|
||||||
|
import net.hzvtc.stat.dto.PopulationEstimateItem;
|
||||||
|
import net.hzvtc.stat.dto.PredictionResult;
|
||||||
|
import net.hzvtc.stat.dto.RasterResult;
|
||||||
|
import net.hzvtc.stat.dto.SimulationResult;
|
||||||
|
import net.hzvtc.stat.service.CameraTrapStatService;
|
||||||
|
import net.hzvtc.stat.service.StatEsDataService;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cameratrapR 11 个函数的 REST 入口(前端调用)。
|
||||||
|
*
|
||||||
|
* <p>所有 endpoint 均接收 JSON body,数据来源二选一:
|
||||||
|
* <ul>
|
||||||
|
* <li>body 中直接传 <code>data</code> 数组(列字段见各方法注释,用于调试 / 自定义数据);</li>
|
||||||
|
* <li>缺省 <code>data</code> 时按查询参数从 ES 索引 <code>animal_valid_record</code> 加载:
|
||||||
|
* 支持 species_id / speciesInIds / deviceIds / dateFrom / dateTo / checked /
|
||||||
|
* recordType / mediaType 等参数(与报表模块一致),经纬度由 device_id
|
||||||
|
* 关联 device_info 表补齐。</li>
|
||||||
|
* </ul></p>
|
||||||
|
*
|
||||||
|
* <p>返回 JSON 对象包含算法结果,便于前端直接渲染。</p>
|
||||||
|
*
|
||||||
|
* <p>路径前缀:<code>/es/stat</code></p>
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/es/stat")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CameraTrapStatController {
|
||||||
|
|
||||||
|
private final CameraTrapStatService service;
|
||||||
|
|
||||||
|
private final StatEsDataService statEsDataService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据来源:body 中带非空 data 数组则直接使用(调试 / 自定义数据);
|
||||||
|
* 否则按查询参数(species_id / speciesInIds / deviceIds / dateFrom / dateTo / checked 等)
|
||||||
|
* 从 ES 索引 animal_valid_record 加载,并关联 device_info 补齐经纬度。
|
||||||
|
*/
|
||||||
|
private List<CameraPoint> cameraData(JSONObject param) {
|
||||||
|
com.alibaba.fastjson.JSONArray data = param.getJSONArray("data");
|
||||||
|
if (data != null && !data.isEmpty()) {
|
||||||
|
return CameraTrapStatService.toCameraPoints(data);
|
||||||
|
}
|
||||||
|
return statEsDataService.loadCameraPoints(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FootprintPoint> footprintData(JSONObject param) {
|
||||||
|
com.alibaba.fastjson.JSONArray data = param.getJSONArray("data");
|
||||||
|
if (data != null && !data.isEmpty()) {
|
||||||
|
return CameraTrapStatService.toFootprintPoints(data);
|
||||||
|
}
|
||||||
|
return statEsDataService.loadFootprintPoints(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChainRecord> chainData(JSONObject param) {
|
||||||
|
com.alibaba.fastjson.JSONArray data = param.getJSONArray("data");
|
||||||
|
if (data != null && !data.isEmpty()) {
|
||||||
|
return CameraTrapStatService.toChainRecords(data);
|
||||||
|
}
|
||||||
|
return statEsDataService.loadChainRecords(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/plotCamtrap
|
||||||
|
* body: { circleSize: 0.2, pointScatter: 5, data: [{lon,lat,groupSize,date,time}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/plotCamtrap")
|
||||||
|
public JSONObject plotCamtrap(@RequestBody JSONObject param) {
|
||||||
|
double circleSize = getDouble(param, "circleSize", 0.2);
|
||||||
|
double pointScatter = getDouble(param, "pointScatter", 5.0);
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
PlotCamtrap.Result result = service.plotCamtrap(data, circleSize, pointScatter);
|
||||||
|
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray agg = new JSONArray();
|
||||||
|
for (CamtrapAggView p : toAggViews(result)) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("lat", p.lat);
|
||||||
|
o.put("lon", p.lon);
|
||||||
|
o.put("count", p.count);
|
||||||
|
agg.add(o);
|
||||||
|
}
|
||||||
|
JSONArray scatter = new JSONArray();
|
||||||
|
result.getScatterPoints().forEach(p -> {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("lon", p.getLon());
|
||||||
|
o.put("lat", p.getLat());
|
||||||
|
o.put("date", p.getDate());
|
||||||
|
o.put("time", p.getTime());
|
||||||
|
o.put("groupSize", p.getGroupSize());
|
||||||
|
scatter.add(o);
|
||||||
|
});
|
||||||
|
out.put("aggPoints", agg);
|
||||||
|
out.put("scatterPoints", scatter);
|
||||||
|
out.put("circleSize", result.getCircleSize());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/dailyRhythm
|
||||||
|
* body: { data: [{lon,lat,groupSize,date,time}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/dailyRhythm")
|
||||||
|
public JSONObject dailyRhythm(@RequestBody JSONObject param) {
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
List<DensitySeries> series = service.dailyRhythm(data);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (DensitySeries s : series) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("x", s.getX());
|
||||||
|
o.put("y", s.getY());
|
||||||
|
o.put("bandwidth", s.getBandwidth());
|
||||||
|
o.put("label", s.getLabel());
|
||||||
|
arr.add(o);
|
||||||
|
}
|
||||||
|
out.put("series", arr);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/geo2proj
|
||||||
|
* body: { data: [{lon,lat,...}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/geo2proj")
|
||||||
|
public JSONObject geo2proj(@RequestBody JSONObject param) {
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
List<Geo2Proj.UtmPoint> utm = service.geo2proj(data);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
utm.forEach(p -> {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("easting", p.getEasting());
|
||||||
|
o.put("northing", p.getNorthing());
|
||||||
|
arr.add(o);
|
||||||
|
});
|
||||||
|
out.put("points", arr);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/simuCamtrap
|
||||||
|
* body: { species: "Red fox", ind: 10, iteration: 3, data: [...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/simuCamtrap")
|
||||||
|
public JSONObject simuCamtrap(@RequestBody JSONObject param) {
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
String species = param.getString("species");
|
||||||
|
int ind = getInt(param, "ind", 10);
|
||||||
|
int iteration = getInt(param, "iteration", 3);
|
||||||
|
SimulationResult res = service.simuCamtrap(data, species, ind, iteration);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("ind", res.getInd());
|
||||||
|
out.put("cameraCounts", res.getCameraCounts());
|
||||||
|
out.put("cameraLon", res.getCameraLon());
|
||||||
|
out.put("cameraLat", res.getCameraLat());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/predictCamtrap
|
||||||
|
* body: { simu: {ind, cameraCounts, cameraLon, cameraLat}, data: [...], nTree: 1000, seed: 1234 }
|
||||||
|
*/
|
||||||
|
@PostMapping("/predictCamtrap")
|
||||||
|
public JSONObject predictCamtrap(@RequestBody JSONObject param) {
|
||||||
|
// 解析 simu
|
||||||
|
JSONObject simuObj = param.getJSONObject("simu");
|
||||||
|
SimulationResult simu = new SimulationResult();
|
||||||
|
JSONArray indArr = simuObj.getJSONArray("ind");
|
||||||
|
int[] ind = new int[indArr.size()];
|
||||||
|
for (int i = 0; i < ind.length; i++) {
|
||||||
|
ind[i] = indArr.getIntValue(i);
|
||||||
|
}
|
||||||
|
simu.setInd(ind);
|
||||||
|
|
||||||
|
JSONArray counts = simuObj.getJSONArray("cameraCounts");
|
||||||
|
long[][] cc = new long[counts.size()][];
|
||||||
|
for (int i = 0; i < cc.length; i++) {
|
||||||
|
JSONArray row = counts.getJSONArray(i);
|
||||||
|
cc[i] = new long[row.size()];
|
||||||
|
for (int j = 0; j < row.size(); j++) {
|
||||||
|
cc[i][j] = row.getLongValue(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
simu.setCameraCounts(cc);
|
||||||
|
|
||||||
|
JSONArray lonArr = simuObj.getJSONArray("cameraLon");
|
||||||
|
JSONArray latArr = simuObj.getJSONArray("cameraLat");
|
||||||
|
double[] lon = new double[lonArr.size()];
|
||||||
|
double[] lat = new double[latArr.size()];
|
||||||
|
for (int i = 0; i < lon.length; i++) {
|
||||||
|
lon[i] = lonArr.getDoubleValue(i);
|
||||||
|
lat[i] = latArr.getDoubleValue(i);
|
||||||
|
}
|
||||||
|
simu.setCameraLon(lon);
|
||||||
|
simu.setCameraLat(lat);
|
||||||
|
|
||||||
|
List<CameraPoint> real = cameraData(param);
|
||||||
|
int nTree = getInt(param, "nTree", 1000);
|
||||||
|
long seed = getLong(param, "seed", 1234L);
|
||||||
|
|
||||||
|
PredictionResult res = service.predictCamtrap(simu, real, nTree, seed);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("noInd", res.getNoInd());
|
||||||
|
out.put("ciLow", res.getCiLow());
|
||||||
|
out.put("ciHigh", res.getCiHigh());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/plotFootprint
|
||||||
|
* body: { data: [{lon,lat,date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/plotFootprint")
|
||||||
|
public JSONObject plotFootprint(@RequestBody JSONObject param) {
|
||||||
|
List<FootprintPoint> chain = footprintData(param);
|
||||||
|
FootprintTrace trace = service.plotFootprint(chain);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray pts = new JSONArray();
|
||||||
|
trace.getPoints().forEach(p -> {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("lon", p.getLon());
|
||||||
|
o.put("lat", p.getLat());
|
||||||
|
o.put("date", p.getDate());
|
||||||
|
pts.add(o);
|
||||||
|
});
|
||||||
|
out.put("points", pts);
|
||||||
|
out.put("startLon", trace.getStartLon());
|
||||||
|
out.put("startLat", trace.getStartLat());
|
||||||
|
out.put("endLon", trace.getEndLon());
|
||||||
|
out.put("endLat", trace.getEndLat());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/moveBias
|
||||||
|
* body: { scale: 2, data: [{lon,lat,date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/moveBias")
|
||||||
|
public JSONObject moveBias(@RequestBody JSONObject param) {
|
||||||
|
List<FootprintPoint> chain = footprintData(param);
|
||||||
|
int scale = getInt(param, "scale", 2);
|
||||||
|
MoveBiasResult res = service.moveBias(chain, scale);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("meanStepLength", res.getMeanStepLength());
|
||||||
|
out.put("sdStepLength", res.getSdStepLength());
|
||||||
|
out.put("sdAngularDeflection", res.getSdAngularDeflection());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/rowcliffe
|
||||||
|
* body: { r: 0.02, theta: 40, v: 2, duration: 40, data: [...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/rowcliffe")
|
||||||
|
public JSONObject rowcliffe(@RequestBody JSONObject param) {
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
double r = getDouble(param, "r", 0.02);
|
||||||
|
double theta = getDouble(param, "theta", 40);
|
||||||
|
double v = getDouble(param, "v", 2);
|
||||||
|
double duration = getDouble(param, "duration", 40);
|
||||||
|
double[] res = service.rowcliffe(data, r, theta, v, duration);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("density", res[0]);
|
||||||
|
out.put("sd", res[1]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/homeRange
|
||||||
|
* body: { data: [{lon,lat,date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/homeRange")
|
||||||
|
public JSONObject homeRange(@RequestBody JSONObject param) {
|
||||||
|
List<FootprintPoint> chain = footprintData(param);
|
||||||
|
double area = service.homeRange(chain);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("area", area);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/homeRanges
|
||||||
|
* body: { data: [{species, line, lon, lat, date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/homeRanges")
|
||||||
|
public JSONObject homeRanges(@RequestBody JSONObject param) {
|
||||||
|
List<ChainRecord> recs = chainData(param);
|
||||||
|
List<HomeRangeItem> list = service.homeRanges(recs);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
list.forEach(it -> {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("species", it.getSpecies());
|
||||||
|
o.put("area", it.getArea());
|
||||||
|
o.put("sd", it.getSd());
|
||||||
|
o.put("n", it.getN());
|
||||||
|
arr.add(o);
|
||||||
|
});
|
||||||
|
out.put("items", arr);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/chainLength
|
||||||
|
* body: { data: [{lon,lat,date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/chainLength")
|
||||||
|
public JSONObject chainLength(@RequestBody JSONObject param) {
|
||||||
|
List<FootprintPoint> chain = footprintData(param);
|
||||||
|
double dist = service.chainLength(chain);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("distance", dist);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/chainLengths
|
||||||
|
* body: { data: [{species, line, lon, lat, date}, ...] }
|
||||||
|
*/
|
||||||
|
@PostMapping("/chainLengths")
|
||||||
|
public JSONObject chainLengths(@RequestBody JSONObject param) {
|
||||||
|
List<ChainRecord> recs = chainData(param);
|
||||||
|
List<ChainLengthItem> list = service.chainLengths(recs);
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
list.forEach(it -> {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("species", it.getSpecies());
|
||||||
|
o.put("distance", it.getDistance());
|
||||||
|
o.put("distSd", it.getDistSd());
|
||||||
|
o.put("stepL", it.getStepL());
|
||||||
|
o.put("stepNo", it.getStepNo());
|
||||||
|
o.put("n", it.getN());
|
||||||
|
arr.add(o);
|
||||||
|
});
|
||||||
|
out.put("items", arr);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// 报表四步:种群大小估计 → 种群密度估计 → 栖息地面积分析 → 栖息地分布范围
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/populationEstimate(报表步骤 1:种群大小估计)
|
||||||
|
* body: { speciesList: ["Red fox", ...](空 = 全部物种), ind: 10, iteration: 3,
|
||||||
|
* nTree: 500, seed: 1234, data: [{lon,lat,groupSize,date,time,species}, ...] 或 ES 查询参数 }
|
||||||
|
* 返回 items: [{species, noInd, ciLow, ciHigh, individual[], recordCount, cameraCount}]
|
||||||
|
*/
|
||||||
|
@PostMapping("/populationEstimate")
|
||||||
|
public JSONObject populationEstimate(@RequestBody JSONObject param) {
|
||||||
|
List<String> speciesList = new java.util.ArrayList<>();
|
||||||
|
JSONArray spArr = param.getJSONArray("speciesList");
|
||||||
|
if (spArr != null) {
|
||||||
|
for (int i = 0; i < spArr.size(); i++) {
|
||||||
|
speciesList.add(spArr.getString(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
int ind = getInt(param, "ind", 10);
|
||||||
|
int iteration = getInt(param, "iteration", 3);
|
||||||
|
int nTree = getInt(param, "nTree", 500);
|
||||||
|
long seed = getLong(param, "seed", 1234L);
|
||||||
|
List<PopulationEstimateItem> items = service.populationEstimate(data, speciesList, ind, iteration, nTree, seed);
|
||||||
|
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray arr = new JSONArray();
|
||||||
|
for (PopulationEstimateItem it : items) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("species", it.getSpecies());
|
||||||
|
o.put("speciesName", it.getSpeciesName());
|
||||||
|
o.put("noInd", it.getNoInd());
|
||||||
|
o.put("ciLow", it.getCiLow());
|
||||||
|
o.put("ciHigh", it.getCiHigh());
|
||||||
|
JSONArray indArr = new JSONArray();
|
||||||
|
if (it.getIndividual() != null) {
|
||||||
|
for (double v : it.getIndividual()) {
|
||||||
|
indArr.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.put("individual", indArr);
|
||||||
|
o.put("recordCount", it.getRecordCount());
|
||||||
|
o.put("cameraCount", it.getCameraCount());
|
||||||
|
arr.add(o);
|
||||||
|
}
|
||||||
|
out.put("items", arr);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/densityEstimate(报表步骤 2:种群密度估计)
|
||||||
|
* body: { cellSize: 0.01, nTree: 200, seed: 1234, data: [...] 或 ES 查询参数 }
|
||||||
|
* 返回栅格 {nx, ny, x0, y0, cellSize, values[], maxValue, cameraCount, photoCount}
|
||||||
|
*/
|
||||||
|
@PostMapping("/densityEstimate")
|
||||||
|
public JSONObject densityEstimate(@RequestBody JSONObject param) {
|
||||||
|
List<CameraPoint> data = cameraData(param);
|
||||||
|
double cellSize = getDouble(param, "cellSize", 0.01);
|
||||||
|
int nTree = getInt(param, "nTree", 200);
|
||||||
|
long seed = getLong(param, "seed", 1234L);
|
||||||
|
RasterResult raster = service.densityEstimate(data, cellSize, nTree, seed);
|
||||||
|
return rasterToJson(raster);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/habitatArea(报表步骤 3:栖息地面积分析)
|
||||||
|
* body: { threshold: 0(≤0 自动 max-Kappa), raster: {nx,ny,x0,y0,cellSize,values},
|
||||||
|
* data: [...](相机点位 = 出现样点,可用 ES 查询参数代替)}
|
||||||
|
* 返回 {threshold, method, suitableCells, totalCells, areaKm2, binary[], ...栅格几何}
|
||||||
|
*/
|
||||||
|
@PostMapping("/habitatArea")
|
||||||
|
public JSONObject habitatArea(@RequestBody JSONObject param) {
|
||||||
|
RasterResult raster = parseRaster(param.getJSONObject("raster"));
|
||||||
|
List<CameraPoint> presence = cameraData(param);
|
||||||
|
double manual = getDouble(param, "threshold", 0.0);
|
||||||
|
HabitatAreaResult res = service.habitatArea(raster, presence, manual);
|
||||||
|
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
out.put("threshold", res.getThreshold());
|
||||||
|
out.put("method", res.getMethod());
|
||||||
|
out.put("suitableCells", res.getSuitableCells());
|
||||||
|
out.put("totalCells", res.getTotalCells());
|
||||||
|
out.put("areaKm2", res.getAreaKm2());
|
||||||
|
out.put("nx", res.getNx());
|
||||||
|
out.put("ny", res.getNy());
|
||||||
|
out.put("x0", res.getX0());
|
||||||
|
out.put("y0", res.getY0());
|
||||||
|
out.put("cellSize", res.getCellSize());
|
||||||
|
out.put("maxValue", res.getMaxValue());
|
||||||
|
JSONArray bin = new JSONArray();
|
||||||
|
if (res.getBinary() != null) {
|
||||||
|
for (int v : res.getBinary()) {
|
||||||
|
bin.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.put("binary", bin);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /es/stat/habitatRange(报表步骤 4:栖息地分布范围)
|
||||||
|
* body: { habitat: {nx,ny,x0,y0,cellSize,suitableCells,binary[]} }(步骤 3 输出)
|
||||||
|
* 返回 {polygon: [[lon,lat],...], areaKm2, suitableCells, totalCells, suitableRatio}
|
||||||
|
*/
|
||||||
|
@PostMapping("/habitatRange")
|
||||||
|
public JSONObject habitatRange(@RequestBody JSONObject param) {
|
||||||
|
JSONObject h = param.getJSONObject("habitat");
|
||||||
|
if (h == null) {
|
||||||
|
throw new IllegalArgumentException("缺少 habitat 参数(应为 habitatArea 的输出)");
|
||||||
|
}
|
||||||
|
HabitatAreaResult area = new HabitatAreaResult();
|
||||||
|
area.setNx(h.getIntValue("nx"));
|
||||||
|
area.setNy(h.getIntValue("ny"));
|
||||||
|
area.setX0(h.getDoubleValue("x0"));
|
||||||
|
area.setY0(h.getDoubleValue("y0"));
|
||||||
|
area.setCellSize(h.getDoubleValue("cellSize"));
|
||||||
|
area.setSuitableCells(h.getIntValue("suitableCells"));
|
||||||
|
JSONArray binArr = h.getJSONArray("binary");
|
||||||
|
int[] binary = new int[binArr == null ? 0 : binArr.size()];
|
||||||
|
if (binArr != null) {
|
||||||
|
for (int i = 0; i < binary.length; i++) {
|
||||||
|
binary[i] = binArr.getIntValue(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
area.setBinary(binary);
|
||||||
|
|
||||||
|
HabitatRangeResult res = service.habitatRange(area);
|
||||||
|
|
||||||
|
JSONObject out = new JSONObject();
|
||||||
|
JSONArray poly = new JSONArray();
|
||||||
|
if (res.getPolygon() != null) {
|
||||||
|
for (double[] p : res.getPolygon()) {
|
||||||
|
JSONArray pt = new JSONArray();
|
||||||
|
pt.add(p[0]);
|
||||||
|
pt.add(p[1]);
|
||||||
|
poly.add(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 各连通斑块凸包(按面积降序)
|
||||||
|
JSONArray polys = new JSONArray();
|
||||||
|
if (res.getPolygons() != null) {
|
||||||
|
for (double[][] ring : res.getPolygons()) {
|
||||||
|
JSONArray r = new JSONArray();
|
||||||
|
if (ring != null) {
|
||||||
|
for (double[] p : ring) {
|
||||||
|
JSONArray pt = new JSONArray();
|
||||||
|
pt.add(p[0]);
|
||||||
|
pt.add(p[1]);
|
||||||
|
r.add(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
polys.add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.put("polygon", poly);
|
||||||
|
out.put("polygons", polys);
|
||||||
|
out.put("clusterCount", res.getClusterCount());
|
||||||
|
out.put("areaKm2", res.getAreaKm2());
|
||||||
|
out.put("suitableCells", res.getSuitableCells());
|
||||||
|
out.put("totalCells", res.getTotalCells());
|
||||||
|
out.put("suitableRatio", res.getSuitableRatio());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RasterResult → JSON。 */
|
||||||
|
private static JSONObject rasterToJson(RasterResult r) {
|
||||||
|
JSONObject o = new JSONObject();
|
||||||
|
o.put("nx", r.getNx());
|
||||||
|
o.put("ny", r.getNy());
|
||||||
|
o.put("x0", r.getX0());
|
||||||
|
o.put("y0", r.getY0());
|
||||||
|
o.put("cellSize", r.getCellSize());
|
||||||
|
o.put("maxValue", r.getMaxValue());
|
||||||
|
o.put("cameraCount", r.getCameraCount());
|
||||||
|
o.put("photoCount", r.getPhotoCount());
|
||||||
|
JSONArray vals = new JSONArray();
|
||||||
|
if (r.getValues() != null) {
|
||||||
|
for (double v : r.getValues()) {
|
||||||
|
vals.add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.put("values", vals);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 前端传入栅格 JSON → RasterResult。 */
|
||||||
|
private static RasterResult parseRaster(JSONObject r) {
|
||||||
|
if (r == null) {
|
||||||
|
throw new IllegalArgumentException("缺少 raster 参数(应为 densityEstimate 的输出)");
|
||||||
|
}
|
||||||
|
RasterResult raster = new RasterResult();
|
||||||
|
raster.setNx(r.getIntValue("nx"));
|
||||||
|
raster.setNy(r.getIntValue("ny"));
|
||||||
|
raster.setX0(r.getDoubleValue("x0"));
|
||||||
|
raster.setY0(r.getDoubleValue("y0"));
|
||||||
|
raster.setCellSize(r.getDoubleValue("cellSize"));
|
||||||
|
raster.setMaxValue(r.getDoubleValue("maxValue"));
|
||||||
|
JSONArray vals = r.getJSONArray("values");
|
||||||
|
double[] values = new double[vals == null ? 0 : vals.size()];
|
||||||
|
if (vals != null) {
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
values[i] = vals.getDoubleValue(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raster.setValues(values);
|
||||||
|
return raster;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// fastjson 1.x 兼容辅助(getDoubleValue(key, def) 等带默认值重载是 fastjson2 API)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
private static double getDouble(JSONObject o, String key, double def) {
|
||||||
|
Double v = o.getDouble(key);
|
||||||
|
return v == null ? def : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getInt(JSONObject o, String key, int def) {
|
||||||
|
Integer v = o.getInteger(key);
|
||||||
|
return v == null ? def : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getLong(JSONObject o, String key, long def) {
|
||||||
|
Long v = o.getLong(key);
|
||||||
|
return v == null ? def : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 聚合点位视图(避免在 lambda 里再构造 JSONObject 嵌套)。 */
|
||||||
|
private static List<CamtrapAggView> toAggViews(PlotCamtrap.Result result) {
|
||||||
|
List<CamtrapAggView> views = new java.util.ArrayList<>(result.getAggPoints().size());
|
||||||
|
result.getAggPoints().forEach(p -> views.add(new CamtrapAggView(p.getLat(), p.getLon(), p.getCount())));
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CamtrapAggView {
|
||||||
|
private final double lat;
|
||||||
|
private final double lon;
|
||||||
|
private final long count;
|
||||||
|
|
||||||
|
private CamtrapAggView(double lat, double lon, long count) {
|
||||||
|
this.lat = lat;
|
||||||
|
this.lon = lon;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.hzvtc.stat.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** Camera-trap movement parameters loaded from MySQL. */
|
||||||
|
@Data
|
||||||
|
public class SpeciesMovementParam {
|
||||||
|
|
||||||
|
private double stepL;
|
||||||
|
private double bias;
|
||||||
|
private double rangeKm2;
|
||||||
|
private int ind;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cameratrapR 中 trapresult 数据框的 Java 表示(plotCamtrap / Rowcliffe / simuCamtrap / predictCamtrap 的输入)。
|
||||||
|
*
|
||||||
|
* <p>对应 R data.frame 列:<code>Lon</code>、<code>Lat</code>、<code>Group_size</code>、<code>Date</code>、<code>Time</code>。</p>
|
||||||
|
*
|
||||||
|
* <p>各字段语义:
|
||||||
|
* <ul>
|
||||||
|
* <li>Lon / Lat:相机拍摄位点的经纬度(度,WGS84)</li>
|
||||||
|
* <li>GroupSize:本条记录对应物种的群体大小(每张照片/视频中个体数)</li>
|
||||||
|
* <li>Date:拍摄日期(yyyy-MM-dd)</li>
|
||||||
|
* <li>Time:拍摄时间(HH:mm:ss)</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CameraPoint {
|
||||||
|
|
||||||
|
/** 经度(度)。 */
|
||||||
|
private double lon;
|
||||||
|
|
||||||
|
/** 纬度(度)。 */
|
||||||
|
private double lat;
|
||||||
|
|
||||||
|
/** 群体大小(单张记录中的个体数)。 */
|
||||||
|
private int groupSize;
|
||||||
|
|
||||||
|
/** 日期,字符串 yyyy-MM-dd。 */
|
||||||
|
private String date;
|
||||||
|
|
||||||
|
/** 时间,字符串 HH:mm:ss。 */
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/** 物种名(ES scientific_name / CSV species 列;多物种统计与密度估计用,可为空)。 */
|
||||||
|
private String species;
|
||||||
|
|
||||||
|
/** 物种中文名(ES species_name / CSV speciesName 列;物种过滤时与学名共同参与匹配,可为空)。 */
|
||||||
|
private String speciesName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* plotCamtrap 输出:按 (Lat, Lon) 聚合后的相机点位(每个点位上的照片总次数 Count)。
|
||||||
|
*
|
||||||
|
* <p>对应 R 中 <code>sum = aggregate(x$Group_size, by=list(x$Lat,x$Lon), sum)</code> 的输出。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CamtrapAggPoint {
|
||||||
|
|
||||||
|
/** 纬度(度)。 */
|
||||||
|
private double lat;
|
||||||
|
|
||||||
|
/** 经度(度)。 */
|
||||||
|
private double lon;
|
||||||
|
|
||||||
|
/** 该点位累计的 GroupSize 总和(用于决定绘图时圆点大小:<code>cex=Count*circle.size</code>)。 */
|
||||||
|
private long count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* plotCamtrap 输出:单条记录的散点(jitter 后)坐标。
|
||||||
|
*
|
||||||
|
* <p>对应 R 中 <code>jitter(X$Lon, factor=point.scatter)</code> / <code>jitter(X$Lat, factor=point.scatter)</code>。</p>
|
||||||
|
*
|
||||||
|
* <p>每个原始记录(Group_size > 0)都会产出一个 jitter 后坐标点;前端画散点。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CamtrapScatter {
|
||||||
|
|
||||||
|
/** jitter 后的经度。 */
|
||||||
|
private double lon;
|
||||||
|
|
||||||
|
/** jitter 后的纬度。 */
|
||||||
|
private double lat;
|
||||||
|
|
||||||
|
/** 原始日期(用于按时间上色)。 */
|
||||||
|
private String date;
|
||||||
|
|
||||||
|
/** 原始时间。 */
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/** 原始群体大小。 */
|
||||||
|
private int groupSize;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chainLengths 输出行:每个物种的足迹链长度汇总。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ChainLengthItem {
|
||||||
|
|
||||||
|
private String species;
|
||||||
|
|
||||||
|
/** 平均链长(km)。 */
|
||||||
|
private double distance;
|
||||||
|
|
||||||
|
/** 标准差。 */
|
||||||
|
private double distSd;
|
||||||
|
|
||||||
|
/** 平均步长(m)。 */
|
||||||
|
private double stepL;
|
||||||
|
|
||||||
|
/** 推算步数(30 天)。 */
|
||||||
|
private long stepNo;
|
||||||
|
|
||||||
|
/** 链样本数。 */
|
||||||
|
private int n;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chainDB 行:单条足迹链记录,附 Species、Line 字段,用于多链多物种聚合。
|
||||||
|
*
|
||||||
|
* <p>对应 R 中 chainDB 数据框(multi-species homeRanges / chainLengths 的输入)。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ChainRecord {
|
||||||
|
|
||||||
|
private String species;
|
||||||
|
|
||||||
|
/** 链编号(同物种可有多条链)。 */
|
||||||
|
private int line;
|
||||||
|
|
||||||
|
private double lon;
|
||||||
|
private double lat;
|
||||||
|
|
||||||
|
/** 记录时间字符串。 */
|
||||||
|
private String date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核密度估计(KDE)曲线数据:x[] / y[] 等长数组,用于前端画 density 曲线。
|
||||||
|
*
|
||||||
|
* <p>对应 R 中 <code>density(times, bw)</code> 返回对象的可视化数据。</p>
|
||||||
|
*
|
||||||
|
* <p>字段语义:
|
||||||
|
* <ul>
|
||||||
|
* <li>x:采样点序列(升序)</li>
|
||||||
|
* <li>y:在 x 处的密度值</li>
|
||||||
|
* <li>bandwidth:核密度带宽 bw(参考输出,便于调试)</li>
|
||||||
|
* <li>label:曲线标签(如 "bw=0.3"、"bw=2")</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DensitySeries {
|
||||||
|
|
||||||
|
/** x 采样点序列(升序)。 */
|
||||||
|
private double[] x;
|
||||||
|
|
||||||
|
/** 密度值序列,与 <code>x</code> 等长。 */
|
||||||
|
private double[] y;
|
||||||
|
|
||||||
|
/** 核密度带宽(bandwidth)。 */
|
||||||
|
private double bandwidth;
|
||||||
|
|
||||||
|
/** 曲线标签(用于前端 legend)。 */
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* plotFootprint 输入 / 输出元素:单个足迹链节点(Lon, Lat, Date)。
|
||||||
|
*
|
||||||
|
* <p>R 中 chain 数据框列:<code>Lon</code>、<code>Lat</code>、<code>Date</code>(按时间排序、采样间隔约 1 秒)。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class FootprintPoint {
|
||||||
|
|
||||||
|
private double lon;
|
||||||
|
private double lat;
|
||||||
|
|
||||||
|
/** 记录时间,字符串 yyyy-MM-dd HH:mm:ss。 */
|
||||||
|
private String date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* plotFootprint 输出:折线轨迹 + 起止点高亮。
|
||||||
|
*
|
||||||
|
* <p>前端可用 <code>points</code> 顺序连线,并在 <code>startLon, startLat</code>(蓝色三角)与
|
||||||
|
* <code>endLon, endLat</code>(红色方块)画标记。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class FootprintTrace {
|
||||||
|
|
||||||
|
/** 折线轨迹(按时间顺序)。 */
|
||||||
|
private List<FootprintPoint> points;
|
||||||
|
|
||||||
|
private double startLon;
|
||||||
|
private double startLat;
|
||||||
|
private double endLon;
|
||||||
|
private double endLat;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 栖息地面积分析输出:基于最优阈值的适宜生境二值栅格。
|
||||||
|
*
|
||||||
|
* <p>阈值默认用 max-Kappa 法(以相机所在格为出现样点、背景格为未出现样点,
|
||||||
|
* 遍历候选阈值取 Cohen's Kappa 最大者),每个物种因密度栅格不同而阈值不同;
|
||||||
|
* 也可由前端显式传入阈值覆盖。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class HabitatAreaResult {
|
||||||
|
|
||||||
|
/** 采用的阈值(密度值 ≥ 阈值视为适宜生境)。 */
|
||||||
|
private double threshold;
|
||||||
|
|
||||||
|
/** 阈值确定方法:maxKappa / manual。 */
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
/** 适宜生境格数。 */
|
||||||
|
private int suitableCells;
|
||||||
|
|
||||||
|
/** 总格数。 */
|
||||||
|
private int totalCells;
|
||||||
|
|
||||||
|
/** 适宜生境面积(km²,按格面积 × 格数,格面积随纬度校正)。 */
|
||||||
|
private double areaKm2;
|
||||||
|
|
||||||
|
/** ---- 栅格几何(与输入密度栅格一致,供下一步分布范围使用)---- */
|
||||||
|
private int nx;
|
||||||
|
private int ny;
|
||||||
|
private double x0;
|
||||||
|
private double y0;
|
||||||
|
private double cellSize;
|
||||||
|
|
||||||
|
/** 二值栅格(1 = 适宜生境,0 = 不适宜;行优先,row 0 最南)。 */
|
||||||
|
private int[] binary;
|
||||||
|
|
||||||
|
/** 透传的源密度栅格最大值(前端图例用)。 */
|
||||||
|
private double maxValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 栖息地分布范围输出:按连通斑块分别求最小凸包(不再跨斑块合并,
|
||||||
|
* 避免散布斑块之间的空隙被单一凸包圈入导致范围虚高)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class HabitatRangeResult {
|
||||||
|
|
||||||
|
/** 最大斑块凸包多边形顶点(闭合环,最后一个点 = 第一个点),[i][0]=lon,[i][1]=lat。 */
|
||||||
|
private double[][] polygon;
|
||||||
|
|
||||||
|
/** 各连通斑块凸包多边形(按面积降序;polygon = 其中最大者)。 */
|
||||||
|
private double[][][] polygons;
|
||||||
|
|
||||||
|
/** 连通斑块数。 */
|
||||||
|
private int clusterCount;
|
||||||
|
|
||||||
|
/** 全部斑块凸包面积之和(km²)。 */
|
||||||
|
private double areaKm2;
|
||||||
|
|
||||||
|
/** 适宜生境格数(输入二值栅格中 =1 的格子数)。 */
|
||||||
|
private int suitableCells;
|
||||||
|
|
||||||
|
/** 总格数。 */
|
||||||
|
private int totalCells;
|
||||||
|
|
||||||
|
/** 栖息地面积占分布范围面积的比例(0-1)。 */
|
||||||
|
private double suitableRatio;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* homeRanges 输出行:每个物种的家域面积(km²)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class HomeRangeItem {
|
||||||
|
|
||||||
|
private String species;
|
||||||
|
|
||||||
|
/** 平均家域面积(km²)。 */
|
||||||
|
private double area;
|
||||||
|
|
||||||
|
/** 标准差。 */
|
||||||
|
private double sd;
|
||||||
|
|
||||||
|
/** 链样本数。 */
|
||||||
|
private int n;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* moveBias 输出:步长统计量与转角统计量。
|
||||||
|
*
|
||||||
|
* <p>对应 R 返回:<code>c(mean(dist), sd(dist), sd(delta.th))</code>。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MoveBiasResult {
|
||||||
|
|
||||||
|
/** 步长均值(m)。 */
|
||||||
|
private double meanStepLength;
|
||||||
|
|
||||||
|
/** 步长标准差(m)。 */
|
||||||
|
private double sdStepLength;
|
||||||
|
|
||||||
|
/** 转向角标准差(度)。 */
|
||||||
|
private double sdAngularDeflection;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* populationEstimate 输出项:单物种的种群大小(数量)估计。
|
||||||
|
*
|
||||||
|
* <p>对应 R 中 simuCamtrap + predictCamtrap 组合的输出:
|
||||||
|
* <code>No_ind</code>(估计值)、<code>CI_L</code>(区间下界)、<code>CI_U</code>(区间上界)。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PopulationEstimateItem {
|
||||||
|
|
||||||
|
/** 物种名(ES scientific_name / species_name,CSV species 列;无物种信息时为 "全部")。 */
|
||||||
|
private String species;
|
||||||
|
|
||||||
|
/** 物种中文名(ES species_name;无中文名时回退为 species)。 */
|
||||||
|
private String speciesName;
|
||||||
|
|
||||||
|
/** 估计个体数 No_ind(随机森林树预测中位数)。 */
|
||||||
|
private double noInd;
|
||||||
|
|
||||||
|
/** 95% 置信区间下界 CI_L。 */
|
||||||
|
private double ciLow;
|
||||||
|
|
||||||
|
/** 95% 置信区间上界 CI_U。 */
|
||||||
|
private double ciHigh;
|
||||||
|
|
||||||
|
/** 每棵树的预测值采样(≤200,供箱线图)。 */
|
||||||
|
private double[] individual;
|
||||||
|
|
||||||
|
/** 参与计算的记录数。 */
|
||||||
|
private int recordCount;
|
||||||
|
|
||||||
|
/** 参与计算的相机点位数。 */
|
||||||
|
private int cameraCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* predictCamtrap 输出:随机森林 1000 棵树的预测中位数与 95% 置信区间。
|
||||||
|
*
|
||||||
|
* <p>对应 R 返回:<code>data.frame(No_ind, CI_L, CI_U)</code>。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PredictionResult {
|
||||||
|
|
||||||
|
/** 估计的个体数(中位数)。 */
|
||||||
|
private double noInd;
|
||||||
|
|
||||||
|
/** 95% 置信区间下界。 */
|
||||||
|
private double ciLow;
|
||||||
|
|
||||||
|
/** 95% 置信区间上界。 */
|
||||||
|
private double ciHigh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每棵树的预测个体数(供前端画箱线图;最多 200 个采样值,中位数/CI 仍基于全量树)。
|
||||||
|
*/
|
||||||
|
private double[] individual;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 种群密度估计(随机森林物种分布模型)输出的栅格结果。
|
||||||
|
*
|
||||||
|
* <p>栅格以相机点位分布范围为边界(四周外扩 3 个格宽),行优先存储:
|
||||||
|
* <code>values[row * nx + col]</code>,row 0 = 最南行,col 0 = 最西列。
|
||||||
|
* 每格值域 [0, 1],表示该物种出现在该格子的概率。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RasterResult {
|
||||||
|
|
||||||
|
/** 列数(西 → 东)。 */
|
||||||
|
private int nx;
|
||||||
|
|
||||||
|
/** 行数(南 → 北)。 */
|
||||||
|
private int ny;
|
||||||
|
|
||||||
|
/** 西南角经度(度)。 */
|
||||||
|
private double x0;
|
||||||
|
|
||||||
|
/** 西南角纬度(度)。 */
|
||||||
|
private double y0;
|
||||||
|
|
||||||
|
/** 格宽(度,经纬度同值)。 */
|
||||||
|
private double cellSize;
|
||||||
|
|
||||||
|
/** 每格出现概率(长度 = nx * ny,行优先,row 0 最南)。 */
|
||||||
|
private double[] values;
|
||||||
|
|
||||||
|
/** 栅格中的最大概率值。 */
|
||||||
|
private double maxValue;
|
||||||
|
|
||||||
|
/** 参与建模的相机点位数。 */
|
||||||
|
private int cameraCount;
|
||||||
|
|
||||||
|
/** 参与建模的照片(记录)总数。 */
|
||||||
|
private long photoCount;
|
||||||
|
|
||||||
|
/** 格子中心经度(度)。 */
|
||||||
|
public double cellCenterLon(int col) {
|
||||||
|
return x0 + (col + 0.5) * cellSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格子中心纬度(度)。 */
|
||||||
|
public double cellCenterLat(int row) {
|
||||||
|
return y0 + (row + 0.5) * cellSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 指定格子的值(越界返回 0)。 */
|
||||||
|
public double valueAt(int row, int col) {
|
||||||
|
if (row < 0 || row >= ny || col < 0 || col >= nx) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return values[row * nx + col];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.hzvtc.stat.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* simuCamtrap 输出:模拟结果矩阵。
|
||||||
|
*
|
||||||
|
* <p>对应 R 返回:<code>data.frame(Ind, [每台相机的 Count])</code>。
|
||||||
|
* 行 = ind*iteration 次模拟;列数 = 相机数 + 1(第一列为个体数 Ind)。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SimulationResult {
|
||||||
|
|
||||||
|
/** 每行的个体数(长度 = ind*iteration)。 */
|
||||||
|
private int[] ind;
|
||||||
|
|
||||||
|
/** 每个相机位的拍照次数(外层长度 = 行数;内层长度 = 相机数)。 */
|
||||||
|
private long[][] cameraCounts;
|
||||||
|
|
||||||
|
/** 相机坐标(与 cameraCounts 列对应)。 */
|
||||||
|
private double[] cameraLon;
|
||||||
|
private double[] cameraLat;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package net.hzvtc.stat.mapper;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.domain.SpeciesMovementParam;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface SpeciesMovementParamMapper {
|
||||||
|
|
||||||
|
@Select("SELECT step_length AS stepL, bias_degree AS bias, range_km2 AS rangeKm2, "
|
||||||
|
+ "max_individuals AS ind FROM camera_trap_species_param "
|
||||||
|
+ "WHERE enabled = 1 AND (species_name = #{species} OR scientific_name = #{species} "
|
||||||
|
+ "OR chinese_name = #{species}) LIMIT 1")
|
||||||
|
SpeciesMovementParam selectBySpecies(@Param("species") String species);
|
||||||
|
|
||||||
|
@Select("SELECT step_length AS stepL, bias_degree AS bias, range_km2 AS rangeKm2, "
|
||||||
|
+ "max_individuals AS ind FROM camera_trap_species_param "
|
||||||
|
+ "WHERE enabled = 1 AND species_name = '__DEFAULT__' LIMIT 1")
|
||||||
|
SpeciesMovementParam selectDefault();
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package net.hzvtc.stat.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import net.hzvtc.stat.algorithm.Geo2Proj;
|
||||||
|
import net.hzvtc.stat.algorithm.PlotCamtrap;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.ChainLengthItem;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.DensitySeries;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.FootprintTrace;
|
||||||
|
import net.hzvtc.stat.dto.HabitatAreaResult;
|
||||||
|
import net.hzvtc.stat.dto.HabitatRangeResult;
|
||||||
|
import net.hzvtc.stat.dto.HomeRangeItem;
|
||||||
|
import net.hzvtc.stat.dto.MoveBiasResult;
|
||||||
|
import net.hzvtc.stat.dto.PopulationEstimateItem;
|
||||||
|
import net.hzvtc.stat.dto.PredictionResult;
|
||||||
|
import net.hzvtc.stat.dto.RasterResult;
|
||||||
|
import net.hzvtc.stat.dto.SimulationResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cameratrapR 11 个函数的业务接口。
|
||||||
|
*
|
||||||
|
* <p>实现见 {@link net.hzvtc.stat.service.impl.CameraTrapStatServiceImpl}。</p>
|
||||||
|
*/
|
||||||
|
public interface CameraTrapStatService {
|
||||||
|
|
||||||
|
// ============================== plotCamtrap ==============================
|
||||||
|
|
||||||
|
PlotCamtrap.Result plotCamtrap(List<CameraPoint> data, double circleSize, double pointScatter);
|
||||||
|
|
||||||
|
// ============================== dailyRhythm ==============================
|
||||||
|
|
||||||
|
List<DensitySeries> dailyRhythm(List<CameraPoint> data);
|
||||||
|
|
||||||
|
// ============================== geo2proj ==============================
|
||||||
|
|
||||||
|
List<Geo2Proj.UtmPoint> geo2proj(List<CameraPoint> data);
|
||||||
|
|
||||||
|
List<Geo2Proj.UtmPoint> geo2projFootprint(List<FootprintPoint> data);
|
||||||
|
|
||||||
|
// ============================== simuCamtrap ==============================
|
||||||
|
|
||||||
|
SimulationResult simuCamtrap(List<CameraPoint> data, String species, int ind, int iteration);
|
||||||
|
|
||||||
|
/** 自定义运动参数版本(保留 R 全部参数可调)。 */
|
||||||
|
SimulationResult simuCamtrapCustom(List<CameraPoint> data, String species,
|
||||||
|
double detect, double[] bearing,
|
||||||
|
int stepN, double stepV,
|
||||||
|
double stepL, double biasDeg,
|
||||||
|
double rangeM, int ind, int iteration);
|
||||||
|
|
||||||
|
// ============================== predictCamtrap ==============================
|
||||||
|
|
||||||
|
PredictionResult predictCamtrap(SimulationResult simu, List<CameraPoint> realData,
|
||||||
|
int nTree, long seed);
|
||||||
|
|
||||||
|
// ============================== populationEstimate(报表:种群大小估计) ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多物种种群大小(数量)估计:按 species 分组逐物种执行 simuCamtrap + predictCamtrap。
|
||||||
|
*
|
||||||
|
* @param data 相机记录(含 species 字段)
|
||||||
|
* @param speciesList 物种过滤(空 = 数据中出现的全部物种)
|
||||||
|
* @param ind 单物种模拟最大个体数
|
||||||
|
* @param iteration 模拟迭代次数
|
||||||
|
* @param nTree 随机森林树数
|
||||||
|
* @param seed 随机种子
|
||||||
|
*/
|
||||||
|
List<PopulationEstimateItem> populationEstimate(List<CameraPoint> data, List<String> speciesList,
|
||||||
|
int ind, int iteration, int nTree, long seed);
|
||||||
|
|
||||||
|
// ============================== densityEstimate(报表:种群密度估计) ==============================
|
||||||
|
|
||||||
|
/** 随机森林 SDM 栅格化出现概率(输入:相机点位 + 积累照片总数)。 */
|
||||||
|
RasterResult densityEstimate(List<CameraPoint> data, double cellSize, int nTree, long seed);
|
||||||
|
|
||||||
|
// ============================== habitatArea(报表:栖息地面积分析) ==============================
|
||||||
|
|
||||||
|
/** 密度栅格 + 最优阈值 → 适宜生境二值栅格(manual ≤ 0 时自动 max-Kappa)。 */
|
||||||
|
HabitatAreaResult habitatArea(RasterResult raster, List<CameraPoint> presence, double manual);
|
||||||
|
|
||||||
|
// ============================== habitatRange(报表:栖息地分布范围) ==============================
|
||||||
|
|
||||||
|
/** 适宜生境二值栅格 → 最小凸包分布范围多边形。 */
|
||||||
|
HabitatRangeResult habitatRange(HabitatAreaResult area);
|
||||||
|
|
||||||
|
// ============================== plotFootprint ==============================
|
||||||
|
|
||||||
|
FootprintTrace plotFootprint(List<FootprintPoint> chain);
|
||||||
|
|
||||||
|
// ============================== moveBias ==============================
|
||||||
|
|
||||||
|
MoveBiasResult moveBias(List<FootprintPoint> chain, int scale);
|
||||||
|
|
||||||
|
// ============================== Rowcliffe ==============================
|
||||||
|
|
||||||
|
double[] rowcliffe(List<CameraPoint> data, double r, double theta, double v, double duration);
|
||||||
|
|
||||||
|
// ============================== homeRange(s) ==============================
|
||||||
|
|
||||||
|
double homeRange(List<FootprintPoint> chain);
|
||||||
|
|
||||||
|
List<HomeRangeItem> homeRanges(List<ChainRecord> records);
|
||||||
|
|
||||||
|
// ============================== chainLength(s) ==============================
|
||||||
|
|
||||||
|
double chainLength(List<FootprintPoint> chain);
|
||||||
|
|
||||||
|
List<ChainLengthItem> chainLengths(List<ChainRecord> records);
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// DTO 转换:JSONArray → List<CameraPoint> / ChainRecord / FootprintPoint
|
||||||
|
// (静态方法委托给实现类,调用方无需感知 Impl)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/** 前端传入 JSONArray(每项含 lon/lat/groupSize/date/time)→ CameraPoint 列表。 */
|
||||||
|
static List<CameraPoint> toCameraPoints(JSONArray arr) {
|
||||||
|
return net.hzvtc.stat.service.impl.CameraTrapStatServiceImpl.toCameraPoints(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<FootprintPoint> toFootprintPoints(JSONArray arr) {
|
||||||
|
return net.hzvtc.stat.service.impl.CameraTrapStatServiceImpl.toFootprintPoints(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ChainRecord> toChainRecords(JSONArray arr) {
|
||||||
|
return net.hzvtc.stat.service.impl.CameraTrapStatServiceImpl.toChainRecords(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package net.hzvtc.stat.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 ES 索引 <code>animal_valid_record</code> 加载数据的业务接口。
|
||||||
|
*
|
||||||
|
* <p>实现见 {@link net.hzvtc.stat.service.impl.StatEsDataServiceImpl}。
|
||||||
|
* 记录关联 <code>device_info</code> 表补齐设备经纬度后,转换为 stat 算法所需 DTO。</p>
|
||||||
|
*
|
||||||
|
* <p>查询参数复用 CommonEsUtils.createQuery,支持:
|
||||||
|
* species_id / speciesInIds / deviceIds / dateFrom / dateTo / checked /
|
||||||
|
* recordType / mediaType / mediaTypes 等(与报表模块参数语义一致)。</p>
|
||||||
|
*/
|
||||||
|
public interface StatEsDataService {
|
||||||
|
|
||||||
|
/** 加载相机记录(plotCamtrap / dailyRhythm / geo2proj / simuCamtrap / rowcliffe 输入)。 */
|
||||||
|
List<CameraPoint> loadCameraPoints(JSONObject param);
|
||||||
|
|
||||||
|
/** 加载足迹链节点(plotFootprint / moveBias / homeRange / chainLength 输入),按时间升序。 */
|
||||||
|
List<FootprintPoint> loadFootprintPoints(JSONObject param);
|
||||||
|
|
||||||
|
/** 加载多链记录(homeRanges / chainLengths 输入),按 species、line、时间排序。 */
|
||||||
|
List<ChainRecord> loadChainRecords(JSONObject param);
|
||||||
|
}
|
||||||
+330
@@ -0,0 +1,330 @@
|
|||||||
|
package net.hzvtc.stat.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.hzvtc.stat.algorithm.ChainLength;
|
||||||
|
import net.hzvtc.stat.algorithm.DailyRhythm;
|
||||||
|
import net.hzvtc.stat.algorithm.DensityEstimate;
|
||||||
|
import net.hzvtc.stat.algorithm.Geo2Proj;
|
||||||
|
import net.hzvtc.stat.algorithm.HabitatAnalysis;
|
||||||
|
import net.hzvtc.stat.algorithm.HabitatRange;
|
||||||
|
import net.hzvtc.stat.algorithm.HomeRange;
|
||||||
|
import net.hzvtc.stat.algorithm.MoveBias;
|
||||||
|
import net.hzvtc.stat.algorithm.PlotCamtrap;
|
||||||
|
import net.hzvtc.stat.algorithm.PlotFootprint;
|
||||||
|
import net.hzvtc.stat.algorithm.PredictCamtrap;
|
||||||
|
import net.hzvtc.stat.algorithm.Rowcliffe;
|
||||||
|
import net.hzvtc.stat.algorithm.SimuCamtrap;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.ChainLengthItem;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.DensitySeries;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.dto.FootprintTrace;
|
||||||
|
import net.hzvtc.stat.dto.HabitatAreaResult;
|
||||||
|
import net.hzvtc.stat.dto.HabitatRangeResult;
|
||||||
|
import net.hzvtc.stat.dto.HomeRangeItem;
|
||||||
|
import net.hzvtc.stat.dto.MoveBiasResult;
|
||||||
|
import net.hzvtc.stat.dto.PopulationEstimateItem;
|
||||||
|
import net.hzvtc.stat.dto.PredictionResult;
|
||||||
|
import net.hzvtc.stat.dto.RasterResult;
|
||||||
|
import net.hzvtc.stat.dto.SimulationResult;
|
||||||
|
import net.hzvtc.stat.domain.SpeciesMovementParam;
|
||||||
|
import net.hzvtc.stat.mapper.SpeciesMovementParamMapper;
|
||||||
|
import net.hzvtc.stat.service.CameraTrapStatService;
|
||||||
|
import net.hzvtc.stat.util.RandomUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link CameraTrapStatService} 实现:调用统计模型,并从 MySQL 加载物种运动参数。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CameraTrapStatServiceImpl implements CameraTrapStatService {
|
||||||
|
|
||||||
|
private final SpeciesMovementParamMapper speciesMovementParamMapper;
|
||||||
|
|
||||||
|
// ============================== plotCamtrap ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlotCamtrap.Result plotCamtrap(List<CameraPoint> data, double circleSize, double pointScatter) {
|
||||||
|
return PlotCamtrap.plotCamtrap(data, circleSize, pointScatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== dailyRhythm ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DensitySeries> dailyRhythm(List<CameraPoint> data) {
|
||||||
|
return DailyRhythm.dailyRhythm(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== geo2proj ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Geo2Proj.UtmPoint> geo2proj(List<CameraPoint> data) {
|
||||||
|
return Geo2Proj.convertToDto(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Geo2Proj.UtmPoint> geo2projFootprint(List<FootprintPoint> data) {
|
||||||
|
double[][] xy = Geo2Proj.convertFootprint(data);
|
||||||
|
List<Geo2Proj.UtmPoint> out = new ArrayList<>(xy.length);
|
||||||
|
for (double[] v : xy) {
|
||||||
|
out.add(new Geo2Proj.UtmPoint(v[0], v[1]));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== simuCamtrap ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimulationResult simuCamtrap(List<CameraPoint> data, String species, int ind, int iteration) {
|
||||||
|
RandomUtils.setSeed(RandomUtils.DEFAULT_SEED);
|
||||||
|
return simulateBySpecies(data, species, ind, iteration);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimulationResult simuCamtrapCustom(List<CameraPoint> data, String species,
|
||||||
|
double detect, double[] bearing,
|
||||||
|
int stepN, double stepV,
|
||||||
|
double stepL, double biasDeg,
|
||||||
|
double rangeM, int ind, int iteration) {
|
||||||
|
return SimuCamtrap.simuCamtrap(data, species, detect, bearing, stepN, stepV,
|
||||||
|
stepL, biasDeg, rangeM, ind, iteration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== predictCamtrap ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PredictionResult predictCamtrap(SimulationResult simu, List<CameraPoint> realData,
|
||||||
|
int nTree, long seed) {
|
||||||
|
return PredictCamtrap.predictCamtrap(simu, realData, nTree, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== populationEstimate ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PopulationEstimateItem> populationEstimate(List<CameraPoint> data, List<String> speciesList,
|
||||||
|
int ind, int iteration, int nTree, long seed) {
|
||||||
|
List<PopulationEstimateItem> items = new ArrayList<>();
|
||||||
|
if (data == null || data.isEmpty()) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 species 分组(保持数据出现顺序)
|
||||||
|
Map<String, List<CameraPoint>> groups = new LinkedHashMap<>();
|
||||||
|
for (CameraPoint p : data) {
|
||||||
|
String sp = p.getSpecies() == null ? "" : p.getSpecies().trim();
|
||||||
|
groups.computeIfAbsent(sp, k -> new ArrayList<>()).add(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物种过滤:学名(species)与中文名(speciesName)共同参与匹配(忽略大小写)
|
||||||
|
Set<String> filter = null;
|
||||||
|
if (speciesList != null) {
|
||||||
|
filter = new LinkedHashSet<>();
|
||||||
|
for (String s : speciesList) {
|
||||||
|
if (s != null && !s.trim().isEmpty()) {
|
||||||
|
filter.add(s.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<CameraPoint>> e : groups.entrySet()) {
|
||||||
|
if (filter != null && !filter.isEmpty() && !matchSpecies(e.getValue(), filter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<CameraPoint> group = e.getValue();
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CameraPoint first = group.get(0);
|
||||||
|
|
||||||
|
// 每个物种用独立派生种子重置模拟随机源(等价 R set.seed),保证同条件结果可复现
|
||||||
|
RandomUtils.setSeed(seed * 1_000_003L + e.getKey().hashCode());
|
||||||
|
SimulationResult simu = simulateBySpecies(group, e.getKey(), ind, iteration);
|
||||||
|
PredictionResult pred = PredictCamtrap.predictCamtrap(simu, group, nTree, seed);
|
||||||
|
|
||||||
|
// 中文名:取组内第一条记录的 species_name,无则回退为学名
|
||||||
|
String spName = first.getSpeciesName();
|
||||||
|
String speciesKey = e.getKey().isEmpty() ? "全部" : e.getKey();
|
||||||
|
if (spName == null || spName.trim().isEmpty()) {
|
||||||
|
spName = speciesKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cameraN = simu.getCameraLon() == null ? 0 : simu.getCameraLon().length;
|
||||||
|
items.add(new PopulationEstimateItem(
|
||||||
|
speciesKey, spName,
|
||||||
|
pred.getNoInd(), pred.getCiLow(), pred.getCiHigh(),
|
||||||
|
pred.getIndividual(), group.size(), cameraN));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationResult simulateBySpecies(List<CameraPoint> data, String species, int ind, int iteration) {
|
||||||
|
SpeciesMovementParam param = null;
|
||||||
|
if (species != null && !species.trim().isEmpty()) {
|
||||||
|
param = speciesMovementParamMapper.selectBySpecies(species.trim());
|
||||||
|
}
|
||||||
|
if (param == null) {
|
||||||
|
param = speciesMovementParamMapper.selectDefault();
|
||||||
|
}
|
||||||
|
if (param == null) {
|
||||||
|
throw new IllegalStateException("camera_trap_species_param is missing the __DEFAULT__ row");
|
||||||
|
}
|
||||||
|
return SimuCamtrap.simuCamtrap(data, species, 50.0, null, 5000, 2.0,
|
||||||
|
param.getStepL(), param.getBias(), param.getRangeKm2() * 1000.0, ind, iteration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断分组内任一记录的学名或中文名命中过滤集合(忽略大小写)。 */
|
||||||
|
private static boolean matchSpecies(List<CameraPoint> group, Set<String> filter) {
|
||||||
|
for (CameraPoint p : group) {
|
||||||
|
if (p.getSpecies() != null && filter.contains(p.getSpecies().trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (filter.stream().anyMatch(f -> p.getSpeciesName() != null
|
||||||
|
&& f.equalsIgnoreCase(p.getSpeciesName().trim()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (p.getSpecies() != null && filter.stream().anyMatch(f -> f.equalsIgnoreCase(p.getSpecies().trim()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== densityEstimate ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RasterResult densityEstimate(List<CameraPoint> data, double cellSize, int nTree, long seed) {
|
||||||
|
return DensityEstimate.densityEstimate(data, cellSize, nTree, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== habitatArea ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HabitatAreaResult habitatArea(RasterResult raster, List<CameraPoint> presence, double manual) {
|
||||||
|
return HabitatAnalysis.habitatArea(raster, presence, manual);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== habitatRange ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HabitatRangeResult habitatRange(HabitatAreaResult area) {
|
||||||
|
return HabitatRange.habitatRange(area);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== plotFootprint ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FootprintTrace plotFootprint(List<FootprintPoint> chain) {
|
||||||
|
return PlotFootprint.plotFootprint(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== moveBias ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MoveBiasResult moveBias(List<FootprintPoint> chain, int scale) {
|
||||||
|
return MoveBias.moveBias(chain, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Rowcliffe ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double[] rowcliffe(List<CameraPoint> data, double r, double theta, double v, double duration) {
|
||||||
|
return Rowcliffe.rowcliffe(data, r, theta, v, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== homeRange(s) ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double homeRange(List<FootprintPoint> chain) {
|
||||||
|
return HomeRange.homeRange(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<HomeRangeItem> homeRanges(List<ChainRecord> records) {
|
||||||
|
return HomeRange.homeRanges(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== chainLength(s) ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double chainLength(List<FootprintPoint> chain) {
|
||||||
|
return ChainLength.chainLength(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ChainLengthItem> chainLengths(List<ChainRecord> records) {
|
||||||
|
return ChainLength.chainLengths(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// DTO 转换辅助:JSONArray → List<CameraPoint> / ChainRecord / FootprintPoint
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/** 前端传入 JSONArray(每项含 lon/lat/groupSize/date/time)→ CameraPoint 列表。 */
|
||||||
|
public static List<CameraPoint> toCameraPoints(JSONArray arr) {
|
||||||
|
List<CameraPoint> list = new ArrayList<>();
|
||||||
|
if (arr == null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
|
JSONObject o = arr.getJSONObject(i);
|
||||||
|
CameraPoint p = new CameraPoint();
|
||||||
|
p.setLon(o.getDoubleValue("lon"));
|
||||||
|
p.setLat(o.getDoubleValue("lat"));
|
||||||
|
p.setGroupSize(o.getIntValue("groupSize"));
|
||||||
|
p.setDate(o.getString("date"));
|
||||||
|
p.setTime(o.getString("time"));
|
||||||
|
String sp = o.getString("species");
|
||||||
|
p.setSpecies(sp == null ? "" : sp);
|
||||||
|
String spName = o.getString("speciesName");
|
||||||
|
p.setSpeciesName(spName == null ? "" : spName);
|
||||||
|
list.add(p);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<FootprintPoint> toFootprintPoints(JSONArray arr) {
|
||||||
|
List<FootprintPoint> list = new ArrayList<>();
|
||||||
|
if (arr == null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
|
JSONObject o = arr.getJSONObject(i);
|
||||||
|
FootprintPoint p = new FootprintPoint();
|
||||||
|
p.setLon(o.getDoubleValue("lon"));
|
||||||
|
p.setLat(o.getDoubleValue("lat"));
|
||||||
|
p.setDate(o.getString("date"));
|
||||||
|
list.add(p);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ChainRecord> toChainRecords(JSONArray arr) {
|
||||||
|
List<ChainRecord> list = new ArrayList<>();
|
||||||
|
if (arr == null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < arr.size(); i++) {
|
||||||
|
JSONObject o = arr.getJSONObject(i);
|
||||||
|
ChainRecord r = new ChainRecord();
|
||||||
|
r.setSpecies(o.getString("species"));
|
||||||
|
r.setLine(o.getIntValue("line"));
|
||||||
|
r.setLon(o.getDoubleValue("lon"));
|
||||||
|
r.setLat(o.getDoubleValue("lat"));
|
||||||
|
r.setDate(o.getString("date"));
|
||||||
|
list.add(r);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
package net.hzvtc.stat.service.impl;
|
||||||
|
|
||||||
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.elasticsearch._types.FieldValue;
|
||||||
|
import co.elastic.clients.elasticsearch._types.SortOrder;
|
||||||
|
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
|
||||||
|
import co.elastic.clients.elasticsearch.core.SearchRequest;
|
||||||
|
import co.elastic.clients.elasticsearch.core.SearchResponse;
|
||||||
|
import co.elastic.clients.elasticsearch.core.search.Hit;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.hzvtc.device.domain.DeviceInfo;
|
||||||
|
import net.hzvtc.device.service.DeviceInfoService;
|
||||||
|
import net.hzvtc.service.impl.CommonEsUtils;
|
||||||
|
import net.hzvtc.stat.dto.CameraPoint;
|
||||||
|
import net.hzvtc.stat.dto.ChainRecord;
|
||||||
|
import net.hzvtc.stat.dto.FootprintPoint;
|
||||||
|
import net.hzvtc.stat.service.StatEsDataService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link StatEsDataService} 实现:从 ES 索引 <code>animal_valid_record</code>
|
||||||
|
* 全量加载有效记录(search_after 分页),关联 <code>device_info</code> 表补齐经纬度。
|
||||||
|
*
|
||||||
|
* <p>字段映射:
|
||||||
|
* <ul>
|
||||||
|
* <li>lon / lat ← device_info.longitude / latitude(按 device_id 关联,无坐标的记录跳过)</li>
|
||||||
|
* <li>groupSize ← count(缺失按 1)</li>
|
||||||
|
* <li>date / time ← record_time(转为 Asia/Shanghai 时区)</li>
|
||||||
|
* <li>species ← scientific_name(缺失用 species_name)</li>
|
||||||
|
* <li>line ← device_id(同一设备的同物种记录序列视为一条足迹链)</li>
|
||||||
|
* </ul></p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatEsDataServiceImpl implements StatEsDataService {
|
||||||
|
|
||||||
|
private static final String INDEX_NAME = "animal_valid_record";
|
||||||
|
|
||||||
|
private static final int PAGE_SIZE = 1000;
|
||||||
|
|
||||||
|
/** 全量拉取上限,防止误查拖垮服务。 */
|
||||||
|
private static final int MAX_ROWS = 200000;
|
||||||
|
|
||||||
|
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
private final ElasticsearchClient elasticsearchClient;
|
||||||
|
|
||||||
|
private final DeviceInfoService deviceInfoService;
|
||||||
|
|
||||||
|
// ============================== 对外加载方法 ==============================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<CameraPoint> loadCameraPoints(JSONObject param) {
|
||||||
|
List<ObjectNode> rows = searchAll(param);
|
||||||
|
Map<Long, double[]> locations = deviceLocations(rows);
|
||||||
|
List<CameraPoint> list = new ArrayList<>(rows.size());
|
||||||
|
for (ObjectNode row : rows) {
|
||||||
|
double[] xy = locations.get(deviceId(row));
|
||||||
|
LocalDateTime time = recordTime(row);
|
||||||
|
if (xy == null || time == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CameraPoint p = new CameraPoint();
|
||||||
|
p.setLon(xy[0]);
|
||||||
|
p.setLat(xy[1]);
|
||||||
|
p.setGroupSize(intVal(row, "count", 1));
|
||||||
|
p.setDate(time.toLocalDate().toString());
|
||||||
|
p.setTime(time.format(DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||||
|
p.setSpecies(species(row));
|
||||||
|
p.setSpeciesName(speciesName(row));
|
||||||
|
list.add(p);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FootprintPoint> loadFootprintPoints(JSONObject param) {
|
||||||
|
List<ObjectNode> rows = searchAll(param);
|
||||||
|
Map<Long, double[]> locations = deviceLocations(rows);
|
||||||
|
List<FootprintPoint> list = new ArrayList<>(rows.size());
|
||||||
|
for (ObjectNode row : rows) {
|
||||||
|
double[] xy = locations.get(deviceId(row));
|
||||||
|
LocalDateTime time = recordTime(row);
|
||||||
|
if (xy == null || time == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FootprintPoint p = new FootprintPoint();
|
||||||
|
p.setLon(xy[0]);
|
||||||
|
p.setLat(xy[1]);
|
||||||
|
p.setDate(time.format(FMT));
|
||||||
|
list.add(p);
|
||||||
|
}
|
||||||
|
list.sort(Comparator.comparing(FootprintPoint::getDate));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ChainRecord> loadChainRecords(JSONObject param) {
|
||||||
|
List<ObjectNode> rows = searchAll(param);
|
||||||
|
Map<Long, double[]> locations = deviceLocations(rows);
|
||||||
|
List<ChainRecord> list = new ArrayList<>(rows.size());
|
||||||
|
for (ObjectNode row : rows) {
|
||||||
|
double[] xy = locations.get(deviceId(row));
|
||||||
|
LocalDateTime time = recordTime(row);
|
||||||
|
if (xy == null || time == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ChainRecord r = new ChainRecord();
|
||||||
|
r.setSpecies(species(row));
|
||||||
|
Long dev = deviceId(row);
|
||||||
|
r.setLine(dev == null ? 0 : dev.intValue());
|
||||||
|
r.setLon(xy[0]);
|
||||||
|
r.setLat(xy[1]);
|
||||||
|
r.setDate(time.format(FMT));
|
||||||
|
list.add(r);
|
||||||
|
}
|
||||||
|
list.sort(Comparator.comparing(ChainRecord::getSpecies)
|
||||||
|
.thenComparing(ChainRecord::getLine)
|
||||||
|
.thenComparing(ChainRecord::getDate));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== ES 全量查询(search_after 分页) ==============================
|
||||||
|
|
||||||
|
private List<ObjectNode> searchAll(JSONObject param) {
|
||||||
|
try {
|
||||||
|
BoolQuery bool = CommonEsUtils.createQuery(param).build();
|
||||||
|
List<ObjectNode> rows = new ArrayList<>();
|
||||||
|
List<FieldValue> searchAfter = null;
|
||||||
|
while (rows.size() < MAX_ROWS) {
|
||||||
|
SearchRequest.Builder rb = new SearchRequest.Builder()
|
||||||
|
.index(INDEX_NAME)
|
||||||
|
.size(PAGE_SIZE)
|
||||||
|
.query(q -> q.bool(bool))
|
||||||
|
.sort(s -> s.field(f -> f.field("id").order(SortOrder.Asc)));
|
||||||
|
if (searchAfter != null) {
|
||||||
|
rb.searchAfter(searchAfter);
|
||||||
|
}
|
||||||
|
SearchResponse<ObjectNode> resp = elasticsearchClient.search(rb.build(), ObjectNode.class);
|
||||||
|
List<Hit<ObjectNode>> hits = resp.hits().hits();
|
||||||
|
if (hits.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (Hit<ObjectNode> h : hits) {
|
||||||
|
if (h.source() != null) {
|
||||||
|
rows.add(h.source());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hits.size() < PAGE_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
searchAfter = hits.get(hits.size() - 1).sort();
|
||||||
|
}
|
||||||
|
if (rows.size() >= MAX_ROWS) {
|
||||||
|
log.warn("stat ES 查询命中数达到上限 {},结果可能被截断", MAX_ROWS);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("查询 ES 索引 " + INDEX_NAME + " 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== 设备坐标关联 ==============================
|
||||||
|
|
||||||
|
/** 青海省经纬度范围(含少量边界余量): lon 89.4~103.2, lat 31.4~39.4。 */
|
||||||
|
private static final double QINGHAI_LON_MIN = 89.4;
|
||||||
|
private static final double QINGHAI_LON_MAX = 103.2;
|
||||||
|
private static final double QINGHAI_LAT_MIN = 31.4;
|
||||||
|
private static final double QINGHAI_LAT_MAX = 39.4;
|
||||||
|
|
||||||
|
/** 汇总记录中出现的 device_id,逐个查询(本次请求内缓存)取设备坐标。 */
|
||||||
|
private Map<Long, double[]> deviceLocations(List<ObjectNode> rows) {
|
||||||
|
Set<Long> ids = new HashSet<>();
|
||||||
|
for (ObjectNode row : rows) {
|
||||||
|
Long id = deviceId(row);
|
||||||
|
if (id != null) {
|
||||||
|
ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<Long, double[]> map = new HashMap<>(ids.size() * 2);
|
||||||
|
for (Long id : ids) {
|
||||||
|
try {
|
||||||
|
DeviceInfo device = deviceInfoService.selectDeviceInfoById(id);
|
||||||
|
if (device != null && inQinghai(device.getLongitude(), device.getLatitude())) {
|
||||||
|
map.put(id, new double[]{device.getLongitude(), device.getLatitude()});
|
||||||
|
} else {
|
||||||
|
log.debug("设备 {} 不存在或坐标({} , {})不在青海范围内,相关记录被跳过",
|
||||||
|
id, device == null ? null : device.getLongitude(),
|
||||||
|
device == null ? null : device.getLatitude());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询设备 {} 坐标失败:{}", id, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 经纬度非 0 且落在青海省范围内。 */
|
||||||
|
private static boolean inQinghai(double lon, double lat) {
|
||||||
|
return lon > 0 && lat > 0
|
||||||
|
&& lon >= QINGHAI_LON_MIN && lon <= QINGHAI_LON_MAX
|
||||||
|
&& lat >= QINGHAI_LAT_MIN && lat <= QINGHAI_LAT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== 字段解析辅助 ==============================
|
||||||
|
|
||||||
|
private static Long deviceId(ObjectNode row) {
|
||||||
|
JsonNode n = row.get("device_id");
|
||||||
|
return n == null || n.isNull() ? null : n.asLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intVal(ObjectNode row, String field, int def) {
|
||||||
|
JsonNode n = row.get(field);
|
||||||
|
return n == null || n.isNull() ? def : n.asInt(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String species(ObjectNode row) {
|
||||||
|
JsonNode n = row.get("scientific_name");
|
||||||
|
if (n != null && !n.isNull() && !n.asText().trim().isEmpty()) {
|
||||||
|
return n.asText();
|
||||||
|
}
|
||||||
|
n = row.get("species_name");
|
||||||
|
return n == null || n.isNull() ? "" : n.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 物种中文名(species_name),用于物种过滤时与学名共同匹配。 */
|
||||||
|
private static String speciesName(ObjectNode row) {
|
||||||
|
JsonNode n = row.get("species_name");
|
||||||
|
return n == null || n.isNull() ? "" : n.asText().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 record_time(ES date 类型,_source 中可能为
|
||||||
|
* "yyyy-MM-dd HH:mm:ss" / ISO8601 带时区 / epoch 毫秒),
|
||||||
|
* 统一转为 Asia/Shanghai 本地时间。
|
||||||
|
*/
|
||||||
|
private static LocalDateTime recordTime(ObjectNode row) {
|
||||||
|
JsonNode n = row.get("record_time");
|
||||||
|
if (n == null || n.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (n.isNumber()) {
|
||||||
|
return LocalDateTime.ofInstant(Instant.ofEpochMilli(n.longValue()), ZONE);
|
||||||
|
}
|
||||||
|
String s = n.asText().trim();
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 带时区(Z / +08:00)→ 转本地时区
|
||||||
|
if (s.endsWith("Z") || s.matches(".*[+-]\\d{2}:?\\d{2}$")) {
|
||||||
|
try {
|
||||||
|
return OffsetDateTime.parse(s).atZoneSameInstant(ZONE).toLocalDateTime();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 落到通用解析
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 无时区,按本地时间解析
|
||||||
|
String t = s.replace('T', ' ');
|
||||||
|
try {
|
||||||
|
if (t.length() >= 19) {
|
||||||
|
return LocalDateTime.parse(t.substring(0, 19), FMT);
|
||||||
|
}
|
||||||
|
if (t.length() == 16) {
|
||||||
|
return LocalDateTime.parse(t + ":00", FMT);
|
||||||
|
}
|
||||||
|
if (t.length() == 10) {
|
||||||
|
return LocalDate.parse(t).atStartOfDay();
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 未知格式
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package net.hzvtc.stat.util;
|
||||||
|
|
||||||
|
import net.hzvtc.stat.dto.DensitySeries;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核密度估计(Gaussian KDE)。
|
||||||
|
*
|
||||||
|
* <p>cameratrapR 中 {@code density(times, bw)} 调用的就是 R stats::density(默认高斯核)。
|
||||||
|
* 本类按相同语义实现:在等距网格上对样本做高斯核密度估计,返回 (x[], y[])。</p>
|
||||||
|
*
|
||||||
|
* <p>核密度公式:
|
||||||
|
* <pre>
|
||||||
|
* f(x) = (1 / (N * bw * sqrt(2π))) * Σ exp(-0.5 * ((x - xi) / bw)^2)
|
||||||
|
* </pre>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class DensityUtils {
|
||||||
|
|
||||||
|
private DensityUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final double SQRT_2PI = Math.sqrt(2.0 * Math.PI);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 [xmin, xmax] 范围内等距取 n 个点,估计样本的高斯核密度。
|
||||||
|
*
|
||||||
|
* @param samples 原始样本(一维数组)
|
||||||
|
* @param bw 带宽(与 R 中 bw 含义一致;dailyRhythm 使用 bw=0.3 与 bw=2)
|
||||||
|
* @param n 输出采样点数(默认 512 与 R 一致)
|
||||||
|
* @param xmin x 轴下限
|
||||||
|
* @param xmax x 轴上限
|
||||||
|
* @param label 曲线标签(写入结果)
|
||||||
|
* @return {@link DensitySeries}
|
||||||
|
*/
|
||||||
|
public static DensitySeries gaussianKde(double[] samples, double bw, int n, double xmin, double xmax, String label) {
|
||||||
|
if (samples.length == 0 || bw <= 0 || n < 2) {
|
||||||
|
return new DensitySeries(new double[0], new double[0], bw, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] x = new double[n];
|
||||||
|
double step = (xmax - xmin) / (n - 1);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
x[i] = xmin + step * i;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] y = new double[n];
|
||||||
|
double norm = 1.0 / (samples.length * bw * SQRT_2PI);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
double xi = x[i];
|
||||||
|
double acc = 0.0;
|
||||||
|
for (double s : samples) {
|
||||||
|
double z = (xi - s) / bw;
|
||||||
|
acc += Math.exp(-0.5 * z * z);
|
||||||
|
}
|
||||||
|
y[i] = acc * norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DensitySeries(x, y, bw, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package net.hzvtc.stat.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地理坐标工具:WGS84 经纬度 → UTM 米。
|
||||||
|
*
|
||||||
|
* <p>cameratrapR 的 <code>geo2proj</code> 默认使用 WGS84 → UTM(按数据经度均值自动推算 zone)。
|
||||||
|
* 本类实现相同的转换,使用 WGS84 椭球参数与 Transverse Mercator(Kruger)公式。</p>
|
||||||
|
*
|
||||||
|
* <p>UTM zone 推算(与 R 代码一致):<code>zone = floor((mean(lon)+180)/6) %% 60 + 1</code>。</p>
|
||||||
|
*/
|
||||||
|
public final class GeoUtils {
|
||||||
|
|
||||||
|
private GeoUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WGS84 长半轴(m)。 */
|
||||||
|
private static final double A = 6378137.0;
|
||||||
|
|
||||||
|
/** WGS84 第一偏心率平方。 */
|
||||||
|
private static final double E2 = 0.00669437999014;
|
||||||
|
|
||||||
|
/** WGS84 第二偏心率平方。 */
|
||||||
|
private static final double EP2 = E2 / (1.0 - E2);
|
||||||
|
|
||||||
|
/** UTM 比例因子。 */
|
||||||
|
private static final double K0 = 0.9996;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将经纬度转换为 UTM 米。
|
||||||
|
*
|
||||||
|
* @param lonDeg 经度(度)
|
||||||
|
* @param latDeg 纬度(度)
|
||||||
|
* @param zone UTM 区号(1..60)
|
||||||
|
* @return double[2] = {easting(m), northing(m)},东向已加 500000 偏移。
|
||||||
|
*/
|
||||||
|
public static double[] wgs84ToUtm(double lonDeg, double latDeg, int zone) {
|
||||||
|
double lonRad = Math.toRadians(lonDeg);
|
||||||
|
double latRad = Math.toRadians(latDeg);
|
||||||
|
|
||||||
|
// 中央经线
|
||||||
|
double lon0Rad = Math.toRadians((zone - 1) * 6.0 - 180.0 + 3.0);
|
||||||
|
|
||||||
|
double sinLat = Math.sin(latRad);
|
||||||
|
double cosLat = Math.cos(latRad);
|
||||||
|
double tanLat = Math.tan(latRad);
|
||||||
|
|
||||||
|
double n = A / Math.sqrt(1.0 - E2 * sinLat * sinLat);
|
||||||
|
double t = tanLat * tanLat;
|
||||||
|
double c = EP2 * cosLat * cosLat;
|
||||||
|
double a = cosLat * (lonRad - lon0Rad);
|
||||||
|
|
||||||
|
// 子午线弧长 M
|
||||||
|
double m = A * ((1.0 - E2 / 4.0 - 3.0 * E2 * E2 / 64.0 - 5.0 * E2 * E2 * E2 / 256.0) * latRad
|
||||||
|
- (3.0 * E2 / 8.0 + 3.0 * E2 * E2 / 32.0 + 45.0 * E2 * E2 * E2 / 1024.0) * Math.sin(2.0 * latRad)
|
||||||
|
+ (15.0 * E2 * E2 / 256.0 + 45.0 * E2 * E2 * E2 / 1024.0) * Math.sin(4.0 * latRad)
|
||||||
|
- (35.0 * E2 * E2 * E2 / 3072.0) * Math.sin(6.0 * latRad));
|
||||||
|
|
||||||
|
double easting = K0 * n * (a + (1.0 - t + c) * a * a * a / 6.0
|
||||||
|
+ (5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * EP2) * Math.pow(a, 5) / 120.0)
|
||||||
|
+ 500000.0;
|
||||||
|
|
||||||
|
double northing = K0 * (m + n * tanLat * (a * a / 2.0
|
||||||
|
+ (5.0 - t + 9.0 * c + 4.0 * c * c) * Math.pow(a, 4) / 24.0
|
||||||
|
+ (61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * EP2) * Math.pow(a, 6) / 720.0));
|
||||||
|
|
||||||
|
// 南半球偏移 10 000 000
|
||||||
|
if (latDeg < 0) {
|
||||||
|
northing += 10000000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new double[]{easting, northing};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动按数据经度均值推算 UTM zone,转换所有点为米。
|
||||||
|
*
|
||||||
|
* @param lons 经度数组(度)
|
||||||
|
* @param lats 纬度数组(度)
|
||||||
|
* @return double[n][2] = {easting(m), northing(m)}
|
||||||
|
*/
|
||||||
|
public static double[][] wgs84ToUtmAuto(double[] lons, double[] lats) {
|
||||||
|
if (lons.length != lats.length) {
|
||||||
|
throw new IllegalArgumentException("lons and lats length mismatch");
|
||||||
|
}
|
||||||
|
double meanLon = 0.0;
|
||||||
|
for (double lon : lons) {
|
||||||
|
meanLon += lon;
|
||||||
|
}
|
||||||
|
meanLon /= lons.length;
|
||||||
|
|
||||||
|
int zone = (int) Math.floor((meanLon + 180.0) / 6.0) % 60 + 1;
|
||||||
|
if (zone < 1) {
|
||||||
|
zone = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[][] out = new double[lons.length][2];
|
||||||
|
for (int i = 0; i < lons.length; i++) {
|
||||||
|
out[i] = wgs84ToUtm(lons[i], lats[i], zone);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动 zone 并转换带 (lon, lat) 字段的对象列表。
|
||||||
|
*/
|
||||||
|
public static <T> double[][] wgs84ToUtmAuto(List<T> points, LonGetter<T> lonGetter, LatGetter<T> latGetter) {
|
||||||
|
double[] lons = new double[points.size()];
|
||||||
|
double[] lats = new double[points.size()];
|
||||||
|
for (int i = 0; i < points.size(); i++) {
|
||||||
|
T p = points.get(i);
|
||||||
|
lons[i] = lonGetter.getLon(p);
|
||||||
|
lats[i] = latGetter.getLat(p);
|
||||||
|
}
|
||||||
|
return wgs84ToUtmAuto(lons, lats);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取对象的经度。 */
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface LonGetter<T> {
|
||||||
|
double getLon(T obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取对象的纬度。 */
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface LatGetter<T> {
|
||||||
|
double getLat(T obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package net.hzvtc.stat.util;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机数工具:均匀、高斯(与 R <code>runif / rnorm</code> 语义一致)。
|
||||||
|
*
|
||||||
|
* <p>默认使用固定种子(等价 R 的 <code>set.seed(1234)</code>),
|
||||||
|
* 保证同样输入下结果可复现;可在每次查询前调用 {@link #setSeed(long)} 重置。</p>
|
||||||
|
*/
|
||||||
|
public final class RandomUtils {
|
||||||
|
|
||||||
|
private RandomUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 默认种子(与接口层 seed 默认值一致)。 */
|
||||||
|
public static final long DEFAULT_SEED = 1234L;
|
||||||
|
|
||||||
|
/** 每线程独立的种子化随机源(ThreadLocalRandom 无法指定种子,故用 ThreadLocal<Random>)。 */
|
||||||
|
private static final ThreadLocal<Random> RNG = ThreadLocal.withInitial(() -> new Random(DEFAULT_SEED));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置随机种子(等价 R 的 <code>set.seed</code>)。
|
||||||
|
* 供每次查询/每个物种模拟前调用,保证同样条件下结果可复现。
|
||||||
|
*/
|
||||||
|
public static void setSeed(long seed) {
|
||||||
|
RNG.set(new Random(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** R 的 <code>runif(1, min, max)</code> 等价物。 */
|
||||||
|
public static double uniform(double min, double max) {
|
||||||
|
return RNG.get().nextDouble() * (max - min) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R 的 <code>rnorm(1, mean, sd)</code> 等价物(Box-Muller 变换,基于同一种子源)。
|
||||||
|
*/
|
||||||
|
public static double gaussian(double mean, double sd) {
|
||||||
|
return gauss() * sd + mean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一次性生成 n 个高斯样本。 */
|
||||||
|
public static double[] gaussianSamples(double mean, double sd, int n) {
|
||||||
|
double[] out = new double[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
out[i] = gaussian(mean, sd);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离散采样(按概率权重抽取一个 index)。等价 R 的 <code>sample(x, 1, prob=p)</code>。
|
||||||
|
*
|
||||||
|
* @param weights 非负权重数组
|
||||||
|
* @return 命中的下标
|
||||||
|
*/
|
||||||
|
public static int weightedSample(double[] weights) {
|
||||||
|
double sum = 0.0;
|
||||||
|
for (double w : weights) {
|
||||||
|
sum += w;
|
||||||
|
}
|
||||||
|
double r = uniform(0.0, sum);
|
||||||
|
double acc = 0.0;
|
||||||
|
for (int i = 0; i < weights.length; i++) {
|
||||||
|
acc += weights[i];
|
||||||
|
if (r <= acc) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return weights.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Box-Muller 标准正态样本。 */
|
||||||
|
private static double gauss() {
|
||||||
|
Random r = RNG.get();
|
||||||
|
double u1;
|
||||||
|
do {
|
||||||
|
u1 = r.nextDouble();
|
||||||
|
} while (u1 <= 0.0);
|
||||||
|
double u2 = r.nextDouble();
|
||||||
|
return Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package net.hzvtc.stat.util;
|
||||||
|
|
||||||
|
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述统计工具:mean / sd / quantile(与 R <code>mean / sd / quantile</code> 语义一致)。
|
||||||
|
*
|
||||||
|
* <p>使用 commons-math3 的 {@link DescriptiveStatistics},分位数采用 R type-7(即线性插值)。</p>
|
||||||
|
*/
|
||||||
|
public final class StatisticsUtils {
|
||||||
|
|
||||||
|
private StatisticsUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double mean(double[] values) {
|
||||||
|
DescriptiveStatistics s = new DescriptiveStatistics(values);
|
||||||
|
return s.getMean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double sd(double[] values) {
|
||||||
|
if (values.length < 2) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return Math.sqrt(new DescriptiveStatistics(values).getVariance());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* R type-7 分位数。
|
||||||
|
*/
|
||||||
|
public static double quantile(double[] values, double p) {
|
||||||
|
DescriptiveStatistics s = new DescriptiveStatistics(values);
|
||||||
|
return s.getPercentile(p * 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double max(double[] values) {
|
||||||
|
double m = Double.NEGATIVE_INFINITY;
|
||||||
|
for (double v : values) {
|
||||||
|
if (v > m) {
|
||||||
|
m = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double min(double[] values) {
|
||||||
|
double m = Double.POSITIVE_INFINITY;
|
||||||
|
for (double v : values) {
|
||||||
|
if (v < m) {
|
||||||
|
m = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double[] sort(double[] values) {
|
||||||
|
double[] copy = values.clone();
|
||||||
|
java.util.Arrays.sort(copy);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.hzvtc.sys.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class CustomDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter INPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ZonedDateTime deserialize(JsonParser p, DeserializationContext ctxt)
|
||||||
|
throws IOException, JsonProcessingException {
|
||||||
|
String dateStr = p.getText();
|
||||||
|
return ZonedDateTime.parse(dateStr, INPUT_FORMATTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package net.hzvtc.sys.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class CustomDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter OUTPUT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(ZonedDateTime value, JsonGenerator gen, SerializerProvider serializers)
|
||||||
|
throws IOException {
|
||||||
|
String formattedDate = value.format(OUTPUT_FORMATTER);
|
||||||
|
gen.writeString(formattedDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package net.hzvtc.sys.config;
|
||||||
|
|
||||||
|
import co.elastic.clients.elasticsearch.ElasticsearchClient;
|
||||||
|
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
|
||||||
|
import co.elastic.clients.transport.ElasticsearchTransport;
|
||||||
|
import co.elastic.clients.transport.rest_client.RestClientTransport;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.http.Header;
|
||||||
|
import org.apache.http.HttpHost;
|
||||||
|
import org.apache.http.auth.AuthScope;
|
||||||
|
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||||
|
import org.apache.http.client.CredentialsProvider;
|
||||||
|
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||||
|
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||||
|
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
|
||||||
|
import org.apache.http.message.BasicHeader;
|
||||||
|
import org.apache.http.ssl.SSLContextBuilder;
|
||||||
|
import org.apache.http.ssl.SSLContexts;
|
||||||
|
import org.elasticsearch.client.RestClient;
|
||||||
|
import org.elasticsearch.client.RestClientBuilder;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class ESClientConfig {
|
||||||
|
|
||||||
|
@Value("${spring.elastic.hosts}")
|
||||||
|
private String hosts;
|
||||||
|
|
||||||
|
// @Setter
|
||||||
|
// @Value("${spring.elastic.port}")
|
||||||
|
// private int port;
|
||||||
|
|
||||||
|
@Value("${spring.elastic.http}")
|
||||||
|
private String http;
|
||||||
|
|
||||||
|
@Value("${spring.elastic.apiKey}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
@Value("${spring.elastic.username}")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Value("${spring.elastic.password}")
|
||||||
|
private String passwd;
|
||||||
|
|
||||||
|
|
||||||
|
private static String certName;
|
||||||
|
|
||||||
|
|
||||||
|
@Value("${spring.elastic.certName}")
|
||||||
|
public void setAppKey(String certName) {
|
||||||
|
ESClientConfig.certName = certName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private HttpHost[] toHttpHost() {
|
||||||
|
if (!StringUtils.hasLength(hosts)) {
|
||||||
|
throw new RuntimeException("invalid elasticsearch configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] hostArray = hosts.split(",");
|
||||||
|
HttpHost[] httpHosts = new HttpHost[hostArray.length];
|
||||||
|
HttpHost httpHost;
|
||||||
|
for (int i = 0; i < hostArray.length; i++) {
|
||||||
|
String[] strings = hostArray[i].split(":");
|
||||||
|
httpHost = new HttpHost(strings[0], Integer.parseInt(strings[1]), "https");
|
||||||
|
httpHosts[i] = httpHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpHosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean("clientByPasswd")
|
||||||
|
public ElasticsearchClient clientByPasswd() {
|
||||||
|
ElasticsearchTransport transport = getElasticsearchTransport(username, passwd, toHttpHost());
|
||||||
|
return new ElasticsearchClient(transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SSLContext buildSSLContext() {
|
||||||
|
// ClassPathResource resource = new ClassPathResource("es_ca.crt");
|
||||||
|
ClassPathResource resource = new ClassPathResource(certName);
|
||||||
|
SSLContext sslContext = null;
|
||||||
|
try {
|
||||||
|
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||||
|
Certificate trustedCa;
|
||||||
|
try (InputStream is = resource.getInputStream()) {
|
||||||
|
trustedCa = factory.generateCertificate(is);
|
||||||
|
}
|
||||||
|
KeyStore trustStore = KeyStore.getInstance("JKS");
|
||||||
|
trustStore.load(null, null);
|
||||||
|
trustStore.setCertificateEntry("ca", trustedCa);
|
||||||
|
SSLContextBuilder sslContextBuilder = SSLContexts.custom()
|
||||||
|
.loadTrustMaterial(trustStore, null);
|
||||||
|
sslContext = sslContextBuilder.build();
|
||||||
|
} catch (IOException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException |
|
||||||
|
CertificateException e) {
|
||||||
|
log.error("ES连接认证失败", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sslContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ElasticsearchTransport getElasticsearchTransport(String username, String passwd, HttpHost... hosts) {
|
||||||
|
// 账号密码的配置
|
||||||
|
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||||
|
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, passwd));
|
||||||
|
|
||||||
|
// 自签证书的设置,并且还包含了账号密码
|
||||||
|
RestClientBuilder.HttpClientConfigCallback callback = httpAsyncClientBuilder -> httpAsyncClientBuilder
|
||||||
|
.setSSLContext(buildSSLContext())
|
||||||
|
.setMaxConnTotal(100)
|
||||||
|
.setMaxConnPerRoute(100)
|
||||||
|
.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
|
||||||
|
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
|
||||||
|
.setDefaultCredentialsProvider(credentialsProvider);
|
||||||
|
|
||||||
|
// 用builder创建RestClient对象
|
||||||
|
RestClient client = RestClient
|
||||||
|
.builder(hosts)
|
||||||
|
.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder.setConnectTimeout(5000).setSocketTimeout(120000))
|
||||||
|
.setHttpClientConfigCallback(callback)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new RestClientTransport(client, new JacksonJsonpMapper(JacksonConfiguration.createObjectMapper()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ElasticsearchTransport getElasticsearchTransport(String apiKey, HttpHost... hosts) {
|
||||||
|
// 将ApiKey放入header中
|
||||||
|
Header[] headers = new Header[]{new BasicHeader("Authorization", "ApiKey " + apiKey)};
|
||||||
|
|
||||||
|
// es自签证书的设置
|
||||||
|
RestClientBuilder.HttpClientConfigCallback callback = httpAsyncClientBuilder -> httpAsyncClientBuilder
|
||||||
|
.setSSLContext(buildSSLContext())
|
||||||
|
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
|
||||||
|
|
||||||
|
// 用builder创建RestClient对象
|
||||||
|
RestClient client = RestClient
|
||||||
|
.builder(hosts)
|
||||||
|
.setHttpClientConfigCallback(callback)
|
||||||
|
.setDefaultHeaders(headers)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new RestClientTransport(client, new JacksonJsonpMapper());
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Bean("clientByApiKey")
|
||||||
|
// public ElasticsearchClient clientByApiKey() throws Exception {
|
||||||
|
// ElasticsearchTransport transport = getElasticsearchTransport(apiKey, toHttpHost());
|
||||||
|
// return new ElasticsearchClient(transport);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.hzvtc.sys.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public class JacksonConfiguration {
|
||||||
|
public static ObjectMapper createObjectMapper() {
|
||||||
|
return new ObjectMapper()
|
||||||
|
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
.registerModule(new JavaTimeModule()
|
||||||
|
.addSerializer(ZonedDateTime.class, new CustomDateTimeSerializer())
|
||||||
|
.addDeserializer(ZonedDateTime.class, new CustomDateTimeDeserializer()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?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="net.hzvtc.es.mapper.EsResultRecordDetailsMapper">
|
||||||
|
<resultMap type="EsResultRecordDetailsVo" id="EsResultRecordDetailsResult">
|
||||||
|
<result property="id" column="id"/>
|
||||||
|
<result property="mediaId" column="media_id"/>
|
||||||
|
<result property="recordId" column="record_id"/>
|
||||||
|
<result property="count" column="count"/>
|
||||||
|
<result property="habitat" column="habitat"/>
|
||||||
|
<result property="recordTime1" column="record_time"/>
|
||||||
|
<result property="recordType" column="record_type"/>
|
||||||
|
<result property="deviceId" column="device_id"/>
|
||||||
|
<result property="deviceName" column="device_name"/>
|
||||||
|
<result property="speciesId" column="species_id"/>
|
||||||
|
<result property="confidence" column="confidence"/>
|
||||||
|
<result property="checked" column="checked"/>
|
||||||
|
<result property="duid" column="duid"/>
|
||||||
|
<result property="speciesName" column="species_name"/>
|
||||||
|
<result property="mediaType" column="media_type"/>
|
||||||
|
<result property="mediaUrl" column="media_url"/>
|
||||||
|
<result property="metaUrl" column="meta_url"/>
|
||||||
|
<result property="deptId" column="dept_id"/>
|
||||||
|
<result property="scientificName" column="scientific_name"/>
|
||||||
|
</resultMap>
|
||||||
|
<resultMap type="BirdStatDataVo" id="EchartDataBirdResult">
|
||||||
|
<result property="id" column="id"/>
|
||||||
|
<result property="name" column="name"/>
|
||||||
|
</resultMap>
|
||||||
|
<select id="getAreaBirdSpeciesByParam" resultMap="EchartDataBirdResult">
|
||||||
|
select b.name as name,b.id from protected_area_species c
|
||||||
|
left JOIN bird_species b on c.species_id = b.id
|
||||||
|
where
|
||||||
|
c.is_deleted = 0
|
||||||
|
and
|
||||||
|
c.area_id = #{areaId}
|
||||||
|
<if test="isCommon != null">
|
||||||
|
and c.is_common = #{isCommon}
|
||||||
|
</if>
|
||||||
|
order by c.position desc,c.species_id
|
||||||
|
<if test="limit!=null">
|
||||||
|
limit #{limit}
|
||||||
|
</if>
|
||||||
|
</select>
|
||||||
|
<select id="getDeviceListByParam" resultMap="EchartDataBirdResult">
|
||||||
|
select a.device_name as name, a.id
|
||||||
|
from device_info a
|
||||||
|
where a.is_deleted = 0
|
||||||
|
<if test="userId != null">
|
||||||
|
and id in (select post_id from sys_user_post where user_id=#{params.userId})
|
||||||
|
</if>
|
||||||
|
order by a.id
|
||||||
|
</select>
|
||||||
|
<select id="getResultRecordListForElastic" resultMap="EsResultRecordDetailsResult">
|
||||||
|
select a.*, c.device_name,c.duid,c.dept_id, d.name as species_name, e.media_type, e.media_url, e.meta_url,d.scientific_name
|
||||||
|
from (select a.details_id as id,
|
||||||
|
a.media_id,
|
||||||
|
a.record_id,
|
||||||
|
IF(a.count = NULL, 1, if(a.count = 0, 1, a.count)) as count,
|
||||||
|
a.habitat,
|
||||||
|
b.record_time,
|
||||||
|
b.record_type,
|
||||||
|
b.device_id,
|
||||||
|
a.species_id,
|
||||||
|
IFNULL(a.confidence = NULL, 0.0) as confidence,
|
||||||
|
IFNULL(a.checked, 0) as checked
|
||||||
|
from result_record b
|
||||||
|
INNER JOIN result_record_details a on b.record_id = a.record_id
|
||||||
|
where b.record_id > #{recordId}
|
||||||
|
and a.media_id in (select media_id from result_media where is_deleted != 1)
|
||||||
|
and a.is_deleted = 0) as a
|
||||||
|
left JOIN bird_species d on a.species_id = d.id
|
||||||
|
left join device_info c on a.device_id = c.id
|
||||||
|
inner JOIN result_media e on a.media_id = e.media_id
|
||||||
|
order by a.id
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package net.hzvtc.framework.aspectj;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.BaseEntity;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysRole;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysUser;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import net.hzvtc.common.annotation.DataScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据过滤处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class DataScopeAspect
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 全部数据权限
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE_ALL = "1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定数据权限
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE_CUSTOM = "2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门数据权限
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE_DEPT = "3";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部门及以下数据权限
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅本人数据权限
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE_SELF = "5";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据权限过滤关键字
|
||||||
|
*/
|
||||||
|
public static final String DATA_SCOPE = "dataScope";
|
||||||
|
|
||||||
|
@Before("@annotation(controllerDataScope)")
|
||||||
|
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
|
||||||
|
{
|
||||||
|
clearDataScope(point);
|
||||||
|
handleDataScope(point, controllerDataScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
|
||||||
|
{
|
||||||
|
// 获取当前的用户
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (StringUtils.isNotNull(loginUser))
|
||||||
|
{
|
||||||
|
SysUser currentUser = loginUser.getUser();
|
||||||
|
// 如果是超级管理员,则不过滤数据
|
||||||
|
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
|
||||||
|
{
|
||||||
|
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
|
||||||
|
controllerDataScope.userAlias());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据范围过滤
|
||||||
|
*
|
||||||
|
* @param joinPoint 切点
|
||||||
|
* @param user 用户
|
||||||
|
* @param userAlias 别名
|
||||||
|
*/
|
||||||
|
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
|
||||||
|
{
|
||||||
|
StringBuilder sqlString = new StringBuilder();
|
||||||
|
|
||||||
|
for (SysRole role : user.getRoles())
|
||||||
|
{
|
||||||
|
String dataScope = role.getDataScope();
|
||||||
|
if (DATA_SCOPE_ALL.equals(dataScope))
|
||||||
|
{
|
||||||
|
sqlString = new StringBuilder();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
|
||||||
|
{
|
||||||
|
sqlString.append(StringUtils.format(
|
||||||
|
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
|
||||||
|
role.getRoleId()));
|
||||||
|
}
|
||||||
|
else if (DATA_SCOPE_DEPT.equals(dataScope))
|
||||||
|
{
|
||||||
|
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
|
||||||
|
}
|
||||||
|
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
|
||||||
|
{
|
||||||
|
sqlString.append(StringUtils.format(
|
||||||
|
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
|
||||||
|
deptAlias, user.getDeptId(), user.getDeptId()));
|
||||||
|
}
|
||||||
|
else if (DATA_SCOPE_SELF.equals(dataScope))
|
||||||
|
{
|
||||||
|
if (StringUtils.isNotBlank(userAlias))
|
||||||
|
{
|
||||||
|
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 数据权限为仅本人且没有userAlias别名不查询任何数据
|
||||||
|
sqlString.append(" OR 1=0 ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(sqlString.toString()))
|
||||||
|
{
|
||||||
|
Object params = joinPoint.getArgs()[0];
|
||||||
|
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
|
||||||
|
{
|
||||||
|
BaseEntity baseEntity = (BaseEntity) params;
|
||||||
|
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接权限sql前先清空params.dataScope参数防止注入
|
||||||
|
*/
|
||||||
|
private void clearDataScope(final JoinPoint joinPoint)
|
||||||
|
{
|
||||||
|
Object params = joinPoint.getArgs()[0];
|
||||||
|
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
|
||||||
|
{
|
||||||
|
BaseEntity baseEntity = (BaseEntity) params;
|
||||||
|
baseEntity.getParams().put(DATA_SCOPE, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package net.hzvtc.framework.aspectj;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.framework.datasource.DynamicDataSourceContextHolder;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import net.hzvtc.common.annotation.DataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多数据源处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Order(1)
|
||||||
|
@Component
|
||||||
|
public class DataSourceAspect
|
||||||
|
{
|
||||||
|
protected Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
@Pointcut("@annotation(net.hzvtc.common.annotation.DataSource)"
|
||||||
|
+ "|| @within(net.hzvtc.common.annotation.DataSource)")
|
||||||
|
public void dsPointCut()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("dsPointCut()")
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable
|
||||||
|
{
|
||||||
|
DataSource dataSource = getDataSource(point);
|
||||||
|
|
||||||
|
if (StringUtils.isNotNull(dataSource))
|
||||||
|
{
|
||||||
|
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return point.proceed();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 销毁数据源 在执行方法之后
|
||||||
|
DynamicDataSourceContextHolder.clearDataSourceType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取需要切换的数据源
|
||||||
|
*/
|
||||||
|
public DataSource getDataSource(ProceedingJoinPoint point)
|
||||||
|
{
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
|
||||||
|
if (Objects.nonNull(dataSource))
|
||||||
|
{
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package net.hzvtc.framework.aspectj;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.enums.BusinessStatus;
|
||||||
|
import net.hzvtc.common.enums.HttpMethod;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.AfterReturning;
|
||||||
|
import org.aspectj.lang.annotation.AfterThrowing;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import net.hzvtc.common.annotation.Log;
|
||||||
|
import net.hzvtc.framework.manager.AsyncManager;
|
||||||
|
import net.hzvtc.framework.manager.factory.AsyncFactory;
|
||||||
|
import net.hzvtc.system.domain.SysOperLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志记录处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class LogAspect
|
||||||
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理完请求后执行
|
||||||
|
*
|
||||||
|
* @param joinPoint 切点
|
||||||
|
*/
|
||||||
|
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
|
||||||
|
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
|
||||||
|
{
|
||||||
|
handleLog(joinPoint, controllerLog, null, jsonResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截异常操作
|
||||||
|
*
|
||||||
|
* @param joinPoint 切点
|
||||||
|
* @param e 异常
|
||||||
|
*/
|
||||||
|
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
|
||||||
|
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
|
||||||
|
{
|
||||||
|
handleLog(joinPoint, controllerLog, e, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
// 获取当前的用户
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
|
||||||
|
// *========数据库日志=========*//
|
||||||
|
SysOperLog operLog = new SysOperLog();
|
||||||
|
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
|
||||||
|
// 请求的地址
|
||||||
|
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
||||||
|
operLog.setOperIp(ip);
|
||||||
|
operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
|
||||||
|
if (loginUser != null)
|
||||||
|
{
|
||||||
|
operLog.setOperName(loginUser.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
operLog.setStatus(BusinessStatus.FAIL.ordinal());
|
||||||
|
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
|
||||||
|
}
|
||||||
|
// 设置方法名称
|
||||||
|
String className = joinPoint.getTarget().getClass().getName();
|
||||||
|
String methodName = joinPoint.getSignature().getName();
|
||||||
|
operLog.setMethod(className + "." + methodName + "()");
|
||||||
|
// 设置请求方式
|
||||||
|
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
|
||||||
|
// 处理设置注解上的参数
|
||||||
|
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
|
||||||
|
// 保存数据库
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
|
||||||
|
}
|
||||||
|
catch (Exception exp)
|
||||||
|
{
|
||||||
|
// 记录本地异常日志
|
||||||
|
log.error("==前置通知异常==");
|
||||||
|
log.error("异常信息:{}", exp.getMessage());
|
||||||
|
exp.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取注解中对方法的描述信息 用于Controller层注解
|
||||||
|
*
|
||||||
|
* @param log 日志
|
||||||
|
* @param operLog 操作日志
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
|
||||||
|
{
|
||||||
|
// 设置action动作
|
||||||
|
operLog.setBusinessType(log.businessType().ordinal());
|
||||||
|
// 设置标题
|
||||||
|
operLog.setTitle(log.title());
|
||||||
|
// 设置操作人类别
|
||||||
|
operLog.setOperatorType(log.operatorType().ordinal());
|
||||||
|
// 是否需要保存request,参数和值
|
||||||
|
if (log.isSaveRequestData())
|
||||||
|
{
|
||||||
|
// 获取参数的信息,传入到数据库中。
|
||||||
|
setRequestValue(joinPoint, operLog);
|
||||||
|
}
|
||||||
|
// 是否需要保存response,参数和值
|
||||||
|
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
|
||||||
|
{
|
||||||
|
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求的参数,放到log中
|
||||||
|
*
|
||||||
|
* @param operLog 操作日志
|
||||||
|
* @throws Exception 异常
|
||||||
|
*/
|
||||||
|
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
|
||||||
|
{
|
||||||
|
String requestMethod = operLog.getRequestMethod();
|
||||||
|
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
|
||||||
|
{
|
||||||
|
String params = argsArrayToString(joinPoint.getArgs());
|
||||||
|
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
|
||||||
|
operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数拼装
|
||||||
|
*/
|
||||||
|
private String argsArrayToString(Object[] paramsArray)
|
||||||
|
{
|
||||||
|
String params = "";
|
||||||
|
if (paramsArray != null && paramsArray.length > 0)
|
||||||
|
{
|
||||||
|
for (Object o : paramsArray)
|
||||||
|
{
|
||||||
|
if (StringUtils.isNotNull(o) && !isFilterObject(o))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Object jsonObj = JSON.toJSON(o);
|
||||||
|
params += jsonObj.toString() + " ";
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否需要过滤的对象。
|
||||||
|
*
|
||||||
|
* @param o 对象信息。
|
||||||
|
* @return 如果是需要过滤的对象,则返回true;否则返回false。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public boolean isFilterObject(final Object o)
|
||||||
|
{
|
||||||
|
Class<?> clazz = o.getClass();
|
||||||
|
if (clazz.isArray())
|
||||||
|
{
|
||||||
|
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
|
||||||
|
}
|
||||||
|
else if (Collection.class.isAssignableFrom(clazz))
|
||||||
|
{
|
||||||
|
Collection collection = (Collection) o;
|
||||||
|
for (Object value : collection)
|
||||||
|
{
|
||||||
|
return value instanceof MultipartFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Map.class.isAssignableFrom(clazz))
|
||||||
|
{
|
||||||
|
Map map = (Map) o;
|
||||||
|
for (Object value : map.entrySet())
|
||||||
|
{
|
||||||
|
Map.Entry entry = (Map.Entry) value;
|
||||||
|
return entry.getValue() instanceof MultipartFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||||
|
|| o instanceof BindingResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package net.hzvtc.framework.aspectj;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import net.hzvtc.common.enums.LimitType;
|
||||||
|
import net.hzvtc.common.exception.ServiceException;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.RedisScript;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import net.hzvtc.common.annotation.RateLimiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class RateLimiterAspect
|
||||||
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
||||||
|
|
||||||
|
private RedisTemplate<Object, Object> redisTemplate;
|
||||||
|
|
||||||
|
private RedisScript<Long> limitScript;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
|
||||||
|
{
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setLimitScript(RedisScript<Long> limitScript)
|
||||||
|
{
|
||||||
|
this.limitScript = limitScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before("@annotation(rateLimiter)")
|
||||||
|
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
|
||||||
|
{
|
||||||
|
String key = rateLimiter.key();
|
||||||
|
int time = rateLimiter.time();
|
||||||
|
int count = rateLimiter.count();
|
||||||
|
|
||||||
|
String combineKey = getCombineKey(rateLimiter, point);
|
||||||
|
List<Object> keys = Collections.singletonList(combineKey);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Long number = redisTemplate.execute(limitScript, keys, count, time);
|
||||||
|
if (StringUtils.isNull(number) || number.intValue() > count)
|
||||||
|
{
|
||||||
|
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||||
|
}
|
||||||
|
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
|
||||||
|
}
|
||||||
|
catch (ServiceException e)
|
||||||
|
{
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new RuntimeException("服务器限流异常,请稍候再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
|
||||||
|
{
|
||||||
|
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
|
||||||
|
if (rateLimiter.limitType() == LimitType.IP)
|
||||||
|
{
|
||||||
|
stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-");
|
||||||
|
}
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
Class<?> targetClass = method.getDeclaringClass();
|
||||||
|
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
|
||||||
|
return stringBuffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 程序注解配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
// 表示通过aop框架暴露该代理对象,AopContext能够访问
|
||||||
|
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||||
|
// 指定要扫描的Mapper类的包的路径
|
||||||
|
@MapperScan("net.hzvtc.**.mapper")
|
||||||
|
public class ApplicationConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 时区配置
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
|
||||||
|
{
|
||||||
|
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import com.google.code.kaptcha.impl.DefaultKaptcha;
|
||||||
|
import com.google.code.kaptcha.util.Config;
|
||||||
|
import static com.google.code.kaptcha.Constants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class CaptchaConfig
|
||||||
|
{
|
||||||
|
@Bean(name = "captchaProducer")
|
||||||
|
public DefaultKaptcha getKaptchaBean()
|
||||||
|
{
|
||||||
|
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||||
|
Properties properties = new Properties();
|
||||||
|
// 是否有边框 默认为true 我们可以自己设置yes,no
|
||||||
|
properties.setProperty(KAPTCHA_BORDER, "yes");
|
||||||
|
// 验证码文本字符颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
|
||||||
|
// 验证码图片宽度 默认为200
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
|
||||||
|
// 验证码图片高度 默认为50
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
|
||||||
|
// 验证码文本字符大小 默认为40
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
|
||||||
|
// KAPTCHA_SESSION_KEY
|
||||||
|
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
|
||||||
|
// 验证码文本字符长度 默认为5
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
|
||||||
|
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
|
||||||
|
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
||||||
|
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
|
||||||
|
Config config = new Config(properties);
|
||||||
|
defaultKaptcha.setConfig(config);
|
||||||
|
return defaultKaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "captchaProducerMath")
|
||||||
|
public DefaultKaptcha getKaptchaBeanMath()
|
||||||
|
{
|
||||||
|
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
|
||||||
|
Properties properties = new Properties();
|
||||||
|
// 是否有边框 默认为true 我们可以自己设置yes,no
|
||||||
|
properties.setProperty(KAPTCHA_BORDER, "yes");
|
||||||
|
// 边框颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
|
||||||
|
// 验证码文本字符颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
|
||||||
|
// 验证码图片宽度 默认为200
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
|
||||||
|
// 验证码图片高度 默认为50
|
||||||
|
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
|
||||||
|
// 验证码文本字符大小 默认为40
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
|
||||||
|
// KAPTCHA_SESSION_KEY
|
||||||
|
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
|
||||||
|
// 验证码文本生成器
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "net.hzvtc.framework.config.KaptchaTextCreator");
|
||||||
|
// 验证码文本字符间距 默认为2
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
|
||||||
|
// 验证码文本字符长度 默认为5
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
|
||||||
|
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
||||||
|
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
|
||||||
|
// 验证码噪点颜色 默认为Color.BLACK
|
||||||
|
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
|
||||||
|
// 干扰实现类
|
||||||
|
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
|
||||||
|
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
||||||
|
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
|
||||||
|
Config config = new Config(properties);
|
||||||
|
defaultKaptcha.setConfig(config);
|
||||||
|
return defaultKaptcha;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import net.hzvtc.common.enums.DataSourceType;
|
||||||
|
import net.hzvtc.common.utils.spring.SpringUtils;
|
||||||
|
import net.hzvtc.framework.config.properties.DruidProperties;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import com.alibaba.druid.pool.DruidDataSource;
|
||||||
|
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
|
||||||
|
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
||||||
|
import com.alibaba.druid.util.Utils;
|
||||||
|
import net.hzvtc.framework.datasource.DynamicDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* druid 配置多数据源
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DruidConfig
|
||||||
|
{
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties("spring.datasource.druid.master")
|
||||||
|
public DataSource masterDataSource(DruidProperties druidProperties)
|
||||||
|
{
|
||||||
|
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
|
||||||
|
return druidProperties.dataSource(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties("spring.datasource.druid.slave")
|
||||||
|
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
|
||||||
|
public DataSource slaveDataSource(DruidProperties druidProperties)
|
||||||
|
{
|
||||||
|
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
|
||||||
|
return druidProperties.dataSource(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "dynamicDataSource")
|
||||||
|
@Primary
|
||||||
|
public DynamicDataSource dataSource(DataSource masterDataSource)
|
||||||
|
{
|
||||||
|
Map<Object, Object> targetDataSources = new HashMap<>();
|
||||||
|
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
|
||||||
|
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
|
||||||
|
return new DynamicDataSource(masterDataSource, targetDataSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置数据源
|
||||||
|
*
|
||||||
|
* @param targetDataSources 备选数据源集合
|
||||||
|
* @param sourceName 数据源名称
|
||||||
|
* @param beanName bean名称
|
||||||
|
*/
|
||||||
|
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DataSource dataSource = SpringUtils.getBean(beanName);
|
||||||
|
targetDataSources.put(sourceName, dataSource);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去除监控页面底部的广告
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
|
||||||
|
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
|
||||||
|
{
|
||||||
|
// 获取web监控页面的参数
|
||||||
|
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
|
||||||
|
// 提取common.js的配置路径
|
||||||
|
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
|
||||||
|
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
|
||||||
|
final String filePath = "support/http/resources/js/common.js";
|
||||||
|
// 创建filter进行过滤
|
||||||
|
Filter filter = new Filter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException
|
||||||
|
{
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
// 重置缓冲区,响应头不会被重置
|
||||||
|
response.resetBuffer();
|
||||||
|
// 获取common.js
|
||||||
|
String text = Utils.readFromResource(filePath);
|
||||||
|
// 正则替换banner, 除去底部的广告信息
|
||||||
|
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
|
||||||
|
text = text.replaceAll("powered.*?shrek.wang</a>", "");
|
||||||
|
response.getWriter().write(text);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void destroy()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
|
||||||
|
registrationBean.setFilter(filter);
|
||||||
|
registrationBean.addUrlPatterns(commonJsPattern);
|
||||||
|
return registrationBean;
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.parser.ParserConfig;
|
||||||
|
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JavaType;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||||
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.SerializationException;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis使用FastJson序列化
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
|
||||||
|
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||||
|
|
||||||
|
static {
|
||||||
|
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private Class<T> clazz;
|
||||||
|
|
||||||
|
public FastJson2JsonRedisSerializer(Class<T> clazz) {
|
||||||
|
super();
|
||||||
|
this.clazz = clazz;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] serialize(T t) throws SerializationException {
|
||||||
|
if (t == null) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T deserialize(byte[] bytes) throws SerializationException {
|
||||||
|
if (bytes == null || bytes.length <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String str = new String(bytes, DEFAULT_CHARSET);
|
||||||
|
|
||||||
|
return JSON.parseObject(str, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setObjectMapper(ObjectMapper objectMapper) {
|
||||||
|
Assert.notNull(objectMapper, "'objectMapper' must not be null");
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JavaType getJavaType(Class<?> clazz) {
|
||||||
|
return TypeFactory.defaultInstance().constructType(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
|
|
||||||
|
import net.hzvtc.common.filter.RepeatableFilter;
|
||||||
|
import net.hzvtc.common.filter.XssFilter;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class FilterConfig
|
||||||
|
{
|
||||||
|
@Value("${xss.excludes}")
|
||||||
|
private String excludes;
|
||||||
|
|
||||||
|
@Value("${xss.urlPatterns}")
|
||||||
|
private String urlPatterns;
|
||||||
|
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
|
||||||
|
public FilterRegistrationBean xssFilterRegistration()
|
||||||
|
{
|
||||||
|
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||||
|
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||||
|
registration.setFilter(new XssFilter());
|
||||||
|
registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
|
||||||
|
registration.setName("xssFilter");
|
||||||
|
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
|
||||||
|
Map<String, String> initParameters = new HashMap<String, String>();
|
||||||
|
initParameters.put("excludes", excludes);
|
||||||
|
registration.setInitParameters(initParameters);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean someFilterRegistration()
|
||||||
|
{
|
||||||
|
FilterRegistrationBean registration = new FilterRegistrationBean();
|
||||||
|
registration.setFilter(new RepeatableFilter());
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setName("repeatableFilter");
|
||||||
|
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码文本生成器
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class KaptchaTextCreator extends DefaultTextCreator
|
||||||
|
{
|
||||||
|
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText()
|
||||||
|
{
|
||||||
|
Integer result = 0;
|
||||||
|
Random random = new Random();
|
||||||
|
int x = random.nextInt(10);
|
||||||
|
int y = random.nextInt(10);
|
||||||
|
StringBuilder suChinese = new StringBuilder();
|
||||||
|
int randomoperands = (int) Math.round(Math.random() * 2);
|
||||||
|
if (randomoperands == 0)
|
||||||
|
{
|
||||||
|
result = x * y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("*");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
else if (randomoperands == 1)
|
||||||
|
{
|
||||||
|
if (!(x == 0) && y % x == 0)
|
||||||
|
{
|
||||||
|
result = y / x;
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
suChinese.append("/");
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = x + y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("+");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (randomoperands == 2)
|
||||||
|
{
|
||||||
|
if (x >= y)
|
||||||
|
{
|
||||||
|
result = x - y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("-");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = y - x;
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
suChinese.append("-");
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = x + y;
|
||||||
|
suChinese.append(CNUMBERS[x]);
|
||||||
|
suChinese.append("+");
|
||||||
|
suChinese.append(CNUMBERS[y]);
|
||||||
|
}
|
||||||
|
suChinese.append("=?@" + result);
|
||||||
|
return suChinese.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.apache.ibatis.io.VFS;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.mybatis.spring.SqlSessionFactoryBean;
|
||||||
|
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
|
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||||
|
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||||
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mybatis支持*匹配扫描包
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class MyBatisConfig
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private Environment env;
|
||||||
|
|
||||||
|
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||||
|
|
||||||
|
public static String setTypeAliasesPackage(String typeAliasesPackage)
|
||||||
|
{
|
||||||
|
ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
|
||||||
|
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
|
||||||
|
List<String> allResult = new ArrayList<String>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (String aliasesPackage : typeAliasesPackage.split(","))
|
||||||
|
{
|
||||||
|
List<String> result = new ArrayList<String>();
|
||||||
|
aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
|
||||||
|
+ ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
|
||||||
|
Resource[] resources = resolver.getResources(aliasesPackage);
|
||||||
|
if (resources != null && resources.length > 0)
|
||||||
|
{
|
||||||
|
MetadataReader metadataReader = null;
|
||||||
|
for (Resource resource : resources)
|
||||||
|
{
|
||||||
|
if (resource.isReadable())
|
||||||
|
{
|
||||||
|
metadataReader = metadataReaderFactory.getMetadataReader(resource);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
|
||||||
|
}
|
||||||
|
catch (ClassNotFoundException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.size() > 0)
|
||||||
|
{
|
||||||
|
HashSet<String> hashResult = new HashSet<String>(result);
|
||||||
|
allResult.addAll(hashResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allResult.size() > 0)
|
||||||
|
{
|
||||||
|
typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return typeAliasesPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource[] resolveMapperLocations(String[] mapperLocations)
|
||||||
|
{
|
||||||
|
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
|
||||||
|
List<Resource> resources = new ArrayList<Resource>();
|
||||||
|
if (mapperLocations != null)
|
||||||
|
{
|
||||||
|
for (String mapperLocation : mapperLocations)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Resource[] mappers = resourceResolver.getResources(mapperLocation);
|
||||||
|
resources.addAll(Arrays.asList(mappers));
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources.toArray(new Resource[resources.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
|
||||||
|
{
|
||||||
|
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
|
||||||
|
String mapperLocations = env.getProperty("mybatis.mapperLocations");
|
||||||
|
String configLocation = env.getProperty("mybatis.configLocation");
|
||||||
|
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
|
||||||
|
VFS.addImplClass(SpringBootVFS.class);
|
||||||
|
|
||||||
|
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
|
||||||
|
sessionFactory.setDataSource(dataSource);
|
||||||
|
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
|
||||||
|
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
|
||||||
|
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
|
||||||
|
return sessionFactory.getObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redis配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableCaching
|
||||||
|
public class RedisConfig extends CachingConfigurerSupport
|
||||||
|
{
|
||||||
|
@Bean
|
||||||
|
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||||
|
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
|
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
||||||
|
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||||
|
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
|
||||||
|
serializer.setObjectMapper(mapper);
|
||||||
|
|
||||||
|
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||||
|
template.setKeySerializer(new StringRedisSerializer());
|
||||||
|
template.setValueSerializer(serializer);
|
||||||
|
|
||||||
|
// Hash的key也采用StringRedisSerializer的序列化方式
|
||||||
|
template.setHashKeySerializer(new StringRedisSerializer());
|
||||||
|
template.setHashValueSerializer(serializer);
|
||||||
|
|
||||||
|
template.afterPropertiesSet();
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DefaultRedisScript<Long> limitScript()
|
||||||
|
{
|
||||||
|
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(limitScriptText());
|
||||||
|
redisScript.setResultType(Long.class);
|
||||||
|
return redisScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流脚本
|
||||||
|
*/
|
||||||
|
private String limitScriptText()
|
||||||
|
{
|
||||||
|
return "local key = KEYS[1]\n" +
|
||||||
|
"local count = tonumber(ARGV[1])\n" +
|
||||||
|
"local time = tonumber(ARGV[2])\n" +
|
||||||
|
"local current = redis.call('get', key);\n" +
|
||||||
|
"if current and tonumber(current) > count then\n" +
|
||||||
|
" return tonumber(current);\n" +
|
||||||
|
"end\n" +
|
||||||
|
"current = redis.call('incr', key)\n" +
|
||||||
|
"if tonumber(current) == 1 then\n" +
|
||||||
|
" redis.call('expire', key, time)\n" +
|
||||||
|
"end\n" +
|
||||||
|
"return tonumber(current);";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import net.hzvtc.common.config.AiBirdSysConfig;
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.framework.interceptor.RepeatSubmitInterceptor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class ResourcesConfig implements WebMvcConfigurer {
|
||||||
|
@Autowired
|
||||||
|
private RepeatSubmitInterceptor repeatSubmitInterceptor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
/** 本地文件上传路径 */
|
||||||
|
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
|
||||||
|
.addResourceLocations("file:" + AiBirdSysConfig.getProfile() + "/");
|
||||||
|
|
||||||
|
/** swagger配置 */
|
||||||
|
registry.addResourceHandler("/swagger-ui/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义拦截规则
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨域配置
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public CorsFilter corsFilter() {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
// 设置访问源地址
|
||||||
|
config.addAllowedOriginPattern("*");
|
||||||
|
// 设置访问源请求头
|
||||||
|
config.addAllowedHeader("*");
|
||||||
|
// 设置访问源请求方法
|
||||||
|
config.addAllowedMethod("*");
|
||||||
|
// 有效期 1800秒
|
||||||
|
config.setMaxAge(1800L);
|
||||||
|
// 添加映射路径,拦截一切请求
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
// 返回新的CorsFilter
|
||||||
|
return new CorsFilter(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import net.hzvtc.framework.security.filter.JwtAuthenticationTokenFilter;
|
||||||
|
import net.hzvtc.framework.security.handle.AuthenticationEntryPointImpl;
|
||||||
|
import net.hzvtc.framework.security.handle.LogoutSuccessHandlerImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* spring security配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
|
||||||
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
/**
|
||||||
|
* 自定义用户认证逻辑
|
||||||
|
*/
|
||||||
|
@Autowired
|
||||||
|
private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证失败处理类
|
||||||
|
*/
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationEntryPointImpl unauthorizedHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出处理类
|
||||||
|
*/
|
||||||
|
@Autowired
|
||||||
|
private LogoutSuccessHandlerImpl logoutSuccessHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token认证过滤器
|
||||||
|
*/
|
||||||
|
@Autowired
|
||||||
|
private JwtAuthenticationTokenFilter authenticationTokenFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨域过滤器
|
||||||
|
*/
|
||||||
|
@Autowired
|
||||||
|
private CorsFilter corsFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解决 无法直接注入 AuthenticationManager
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Override
|
||||||
|
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||||
|
return super.authenticationManagerBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* anyRequest | 匹配所有请求路径
|
||||||
|
* access | SpringEl表达式结果为true时可以访问
|
||||||
|
* anonymous | 匿名可以访问
|
||||||
|
* denyAll | 用户不能访问
|
||||||
|
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
|
||||||
|
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
|
||||||
|
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
|
||||||
|
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
|
||||||
|
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
|
||||||
|
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
|
||||||
|
* permitAll | 用户可以任意访问
|
||||||
|
* rememberMe | 允许通过remember-me登录的用户访问
|
||||||
|
* authenticated | 用户登录后可访问
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity httpSecurity) throws Exception {
|
||||||
|
httpSecurity
|
||||||
|
// CSRF禁用,因为不使用session
|
||||||
|
.csrf().disable()
|
||||||
|
// 认证失败处理类
|
||||||
|
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
|
||||||
|
// 基于token,所以不需要session
|
||||||
|
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||||
|
// 过滤请求
|
||||||
|
.authorizeRequests()
|
||||||
|
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
|
||||||
|
.antMatchers("/login", "/web/api/getToken",
|
||||||
|
"/appCaptchaImage",
|
||||||
|
"/captchaImage",
|
||||||
|
"/appUpdate",
|
||||||
|
"/sm2login",
|
||||||
|
"/web/getPublicKey",
|
||||||
|
"/restPassword").anonymous()
|
||||||
|
.antMatchers(
|
||||||
|
HttpMethod.GET,
|
||||||
|
"/",
|
||||||
|
"/*.html",
|
||||||
|
"/**/*.html",
|
||||||
|
"/**/*.css",
|
||||||
|
"/**/*.js",
|
||||||
|
"/profile/**"
|
||||||
|
).permitAll()
|
||||||
|
.antMatchers("/swagger-ui.html").anonymous()
|
||||||
|
.antMatchers("/swagger-resources/**").anonymous()
|
||||||
|
.antMatchers("/webjars/**").anonymous()
|
||||||
|
.antMatchers("/*/api-docs").anonymous()
|
||||||
|
.antMatchers("/druid/**").anonymous()
|
||||||
|
// 除上面外的所有请求全部需要鉴权认证
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.headers().frameOptions().disable();
|
||||||
|
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
|
||||||
|
// 添加JWT filter
|
||||||
|
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
// 添加CORS filter
|
||||||
|
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
|
||||||
|
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强散列哈希加密实现
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public BCryptPasswordEncoder bCryptPasswordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 身份认证接口
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||||
|
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务相关配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ServerConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 获取完整的请求路径,包括:域名,端口,上下文访问路径
|
||||||
|
*
|
||||||
|
* @return 服务地址
|
||||||
|
*/
|
||||||
|
public String getUrl()
|
||||||
|
{
|
||||||
|
HttpServletRequest request = ServletUtils.getRequest();
|
||||||
|
return getDomain(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDomain(HttpServletRequest request)
|
||||||
|
{
|
||||||
|
StringBuffer url = request.getRequestURL();
|
||||||
|
String contextPath = request.getServletContext().getContextPath();
|
||||||
|
return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package net.hzvtc.framework.config;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.Threads;
|
||||||
|
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 线程池配置
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
**/
|
||||||
|
@Configuration
|
||||||
|
public class ThreadPoolConfig
|
||||||
|
{
|
||||||
|
// 核心线程池大小
|
||||||
|
private int corePoolSize = 50;
|
||||||
|
|
||||||
|
// 最大可创建的线程数
|
||||||
|
private int maxPoolSize = 200;
|
||||||
|
|
||||||
|
// 队列最大长度
|
||||||
|
private int queueCapacity = 1000;
|
||||||
|
|
||||||
|
// 线程池维护线程所允许的空闲时间
|
||||||
|
private int keepAliveSeconds = 300;
|
||||||
|
|
||||||
|
@Bean(name = "threadPoolTaskExecutor")
|
||||||
|
public ThreadPoolTaskExecutor threadPoolTaskExecutor()
|
||||||
|
{
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setMaxPoolSize(maxPoolSize);
|
||||||
|
executor.setCorePoolSize(corePoolSize);
|
||||||
|
executor.setQueueCapacity(queueCapacity);
|
||||||
|
executor.setKeepAliveSeconds(keepAliveSeconds);
|
||||||
|
// 线程池对拒绝任务(无线程可用)的处理策略
|
||||||
|
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行周期性或定时任务
|
||||||
|
*/
|
||||||
|
@Bean(name = "scheduledExecutorService")
|
||||||
|
protected ScheduledExecutorService scheduledExecutorService()
|
||||||
|
{
|
||||||
|
return new ScheduledThreadPoolExecutor(corePoolSize,
|
||||||
|
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy())
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void afterExecute(Runnable r, Throwable t)
|
||||||
|
{
|
||||||
|
super.afterExecute(r, t);
|
||||||
|
Threads.printException(r, t);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package net.hzvtc.framework.config.properties;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import com.alibaba.druid.pool.DruidDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* druid 配置属性
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DruidProperties
|
||||||
|
{
|
||||||
|
@Value("${spring.datasource.druid.initialSize}")
|
||||||
|
private int initialSize;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.minIdle}")
|
||||||
|
private int minIdle;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.maxActive}")
|
||||||
|
private int maxActive;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.maxWait}")
|
||||||
|
private int maxWait;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
|
||||||
|
private int timeBetweenEvictionRunsMillis;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
|
||||||
|
private int minEvictableIdleTimeMillis;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
|
||||||
|
private int maxEvictableIdleTimeMillis;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.validationQuery}")
|
||||||
|
private String validationQuery;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.testWhileIdle}")
|
||||||
|
private boolean testWhileIdle;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.testOnBorrow}")
|
||||||
|
private boolean testOnBorrow;
|
||||||
|
|
||||||
|
@Value("${spring.datasource.druid.testOnReturn}")
|
||||||
|
private boolean testOnReturn;
|
||||||
|
|
||||||
|
public DruidDataSource dataSource(DruidDataSource datasource)
|
||||||
|
{
|
||||||
|
/** 配置初始化大小、最小、最大 */
|
||||||
|
datasource.setInitialSize(initialSize);
|
||||||
|
datasource.setMaxActive(maxActive);
|
||||||
|
datasource.setMinIdle(minIdle);
|
||||||
|
|
||||||
|
/** 配置获取连接等待超时的时间 */
|
||||||
|
datasource.setMaxWait(maxWait);
|
||||||
|
|
||||||
|
/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
|
||||||
|
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
|
||||||
|
|
||||||
|
/** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
|
||||||
|
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
|
||||||
|
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
|
||||||
|
*/
|
||||||
|
datasource.setValidationQuery(validationQuery);
|
||||||
|
/** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
|
||||||
|
datasource.setTestWhileIdle(testWhileIdle);
|
||||||
|
/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
|
||||||
|
datasource.setTestOnBorrow(testOnBorrow);
|
||||||
|
/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
|
||||||
|
datasource.setTestOnReturn(testOnReturn);
|
||||||
|
return datasource;
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package net.hzvtc.framework.datasource;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态数据源
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class DynamicDataSource extends AbstractRoutingDataSource
|
||||||
|
{
|
||||||
|
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
|
||||||
|
{
|
||||||
|
super.setDefaultTargetDataSource(defaultTargetDataSource);
|
||||||
|
super.setTargetDataSources(targetDataSources);
|
||||||
|
super.afterPropertiesSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object determineCurrentLookupKey()
|
||||||
|
{
|
||||||
|
return DynamicDataSourceContextHolder.getDataSourceType();
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package net.hzvtc.framework.datasource;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源切换处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class DynamicDataSourceContextHolder
|
||||||
|
{
|
||||||
|
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
|
||||||
|
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置数据源的变量
|
||||||
|
*/
|
||||||
|
public static void setDataSourceType(String dsType)
|
||||||
|
{
|
||||||
|
log.info("切换到{}数据源", dsType);
|
||||||
|
CONTEXT_HOLDER.set(dsType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得数据源的变量
|
||||||
|
*/
|
||||||
|
public static String getDataSourceType()
|
||||||
|
{
|
||||||
|
return CONTEXT_HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空数据源变量
|
||||||
|
*/
|
||||||
|
public static void clearDataSourceType()
|
||||||
|
{
|
||||||
|
CONTEXT_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package net.hzvtc.framework.datasource;
|
||||||
|
|
||||||
|
import com.github.pagehelper.Page;
|
||||||
|
import com.github.pagehelper.dialect.helper.MySqlDialect;
|
||||||
|
import org.apache.ibatis.cache.CacheKey;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class MySqlDialectCustom extends MySqlDialect {
|
||||||
|
@Override
|
||||||
|
public String getPageSql(String sql, Page page, CacheKey pageKey) {
|
||||||
|
Pattern compileFixed = Pattern.compile("/\\*fixed\\*/");
|
||||||
|
String compileLimit = "/\\*limit\\*/";
|
||||||
|
if (compileFixed.matcher(sql).find()) {
|
||||||
|
if (page.getStartRow() == 0) {
|
||||||
|
sql = sql.replaceFirst(compileLimit, " LIMIT ? ");
|
||||||
|
} else {
|
||||||
|
sql = sql.replaceFirst(compileLimit, " LIMIT ?, ? ");
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
return super.getPageSql(sql, page, pageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package net.hzvtc.framework.interceptor;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import net.hzvtc.common.annotation.RepeatSubmit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防止重复提交拦截器
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
|
||||||
|
{
|
||||||
|
if (handler instanceof HandlerMethod)
|
||||||
|
{
|
||||||
|
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||||
|
Method method = handlerMethod.getMethod();
|
||||||
|
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
|
||||||
|
if (annotation != null)
|
||||||
|
{
|
||||||
|
if (this.isRepeatSubmit(request, annotation))
|
||||||
|
{
|
||||||
|
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
|
||||||
|
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证是否重复提交由子类实现具体的防重复提交的规则
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package net.hzvtc.framework.interceptor.impl;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.core.redis.RedisCache;
|
||||||
|
import net.hzvtc.common.filter.RepeatedlyRequestWrapper;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.common.utils.http.HttpHelper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import net.hzvtc.common.annotation.RepeatSubmit;
|
||||||
|
import net.hzvtc.framework.interceptor.RepeatSubmitInterceptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断请求url和数据是否和上一次相同,
|
||||||
|
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
|
||||||
|
{
|
||||||
|
public final String REPEAT_PARAMS = "repeatParams";
|
||||||
|
|
||||||
|
public final String REPEAT_TIME = "repeatTime";
|
||||||
|
|
||||||
|
// 令牌自定义标识
|
||||||
|
@Value("${token.header}")
|
||||||
|
private String header;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
|
||||||
|
{
|
||||||
|
String nowParams = "";
|
||||||
|
if (request instanceof RepeatedlyRequestWrapper)
|
||||||
|
{
|
||||||
|
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
|
||||||
|
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// body参数为空,获取Parameter的数据
|
||||||
|
if (StringUtils.isEmpty(nowParams))
|
||||||
|
{
|
||||||
|
nowParams = JSONObject.toJSONString(request.getParameterMap());
|
||||||
|
}
|
||||||
|
Map<String, Object> nowDataMap = new HashMap<String, Object>();
|
||||||
|
nowDataMap.put(REPEAT_PARAMS, nowParams);
|
||||||
|
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
|
||||||
|
|
||||||
|
// 请求地址(作为存放cache的key值)
|
||||||
|
String url = request.getRequestURI();
|
||||||
|
|
||||||
|
// 唯一值(没有消息头则使用请求地址)
|
||||||
|
String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
|
||||||
|
|
||||||
|
// 唯一标识(指定key + url + 消息头)
|
||||||
|
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + url + submitKey;
|
||||||
|
|
||||||
|
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
|
||||||
|
if (sessionObj != null)
|
||||||
|
{
|
||||||
|
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
|
||||||
|
if (sessionMap.containsKey(url))
|
||||||
|
{
|
||||||
|
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
|
||||||
|
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> cacheMap = new HashMap<String, Object>();
|
||||||
|
cacheMap.put(url, nowDataMap);
|
||||||
|
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断参数是否相同
|
||||||
|
*/
|
||||||
|
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
|
||||||
|
{
|
||||||
|
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
|
||||||
|
String preParams = (String) preMap.get(REPEAT_PARAMS);
|
||||||
|
return nowParams.equals(preParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断两次间隔时间
|
||||||
|
*/
|
||||||
|
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
|
||||||
|
{
|
||||||
|
long time1 = (Long) nowMap.get(REPEAT_TIME);
|
||||||
|
long time2 = (Long) preMap.get(REPEAT_TIME);
|
||||||
|
if ((time1 - time2) < interval)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package net.hzvtc.framework.manager;
|
||||||
|
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import net.hzvtc.common.utils.Threads;
|
||||||
|
import net.hzvtc.common.utils.spring.SpringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步任务管理器
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class AsyncManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 操作延迟10毫秒
|
||||||
|
*/
|
||||||
|
private final int OPERATE_DELAY_TIME = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步操作任务调度线程池
|
||||||
|
*/
|
||||||
|
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单例模式
|
||||||
|
*/
|
||||||
|
private AsyncManager(){}
|
||||||
|
|
||||||
|
private static AsyncManager me = new AsyncManager();
|
||||||
|
|
||||||
|
public static AsyncManager me()
|
||||||
|
{
|
||||||
|
return me;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行任务
|
||||||
|
*
|
||||||
|
* @param task 任务
|
||||||
|
*/
|
||||||
|
public void execute(TimerTask task)
|
||||||
|
{
|
||||||
|
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止任务线程池
|
||||||
|
*/
|
||||||
|
public void shutdown()
|
||||||
|
{
|
||||||
|
Threads.shutdownAndAwaitTermination(executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package net.hzvtc.framework.manager;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保应用退出时能关闭后台线程
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ShutdownManager
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger("sys-user");
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy()
|
||||||
|
{
|
||||||
|
shutdownAsyncManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止异步执行任务
|
||||||
|
*/
|
||||||
|
private void shutdownAsyncManager()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.info("====关闭后台任务任务线程池====");
|
||||||
|
AsyncManager.me().shutdown();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
package net.hzvtc.framework.manager.factory;
|
||||||
|
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.utils.LogUtils;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.AddressUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import net.hzvtc.common.utils.spring.SpringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import net.hzvtc.system.domain.SysLogininfor;
|
||||||
|
import net.hzvtc.system.domain.SysOperLog;
|
||||||
|
import net.hzvtc.system.service.SysLogininforService;
|
||||||
|
import net.hzvtc.system.service.SysOperLogService;
|
||||||
|
import eu.bitwalker.useragentutils.UserAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工厂(产生任务用)
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class AsyncFactory
|
||||||
|
{
|
||||||
|
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录信息
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param status 状态
|
||||||
|
* @param message 消息
|
||||||
|
* @param args 列表
|
||||||
|
* @return 任务task
|
||||||
|
*/
|
||||||
|
public static TimerTask recordLogininfor(final String username, final String status, final String message,
|
||||||
|
final Object... args)
|
||||||
|
{
|
||||||
|
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||||
|
final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
||||||
|
return new TimerTask()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
String address = AddressUtils.getRealAddressByIP(ip);
|
||||||
|
StringBuilder s = new StringBuilder();
|
||||||
|
s.append(LogUtils.getBlock(ip));
|
||||||
|
s.append(address);
|
||||||
|
s.append(LogUtils.getBlock(username));
|
||||||
|
s.append(LogUtils.getBlock(status));
|
||||||
|
s.append(LogUtils.getBlock(message));
|
||||||
|
// 打印信息到日志
|
||||||
|
sys_user_logger.info(s.toString(), args);
|
||||||
|
// 获取客户端操作系统
|
||||||
|
String os = userAgent.getOperatingSystem().getName();
|
||||||
|
// 获取客户端浏览器
|
||||||
|
String browser = userAgent.getBrowser().getName();
|
||||||
|
// 封装对象
|
||||||
|
SysLogininfor logininfor = new SysLogininfor();
|
||||||
|
logininfor.setUserName(username);
|
||||||
|
logininfor.setIpaddr(ip);
|
||||||
|
logininfor.setLoginLocation(address);
|
||||||
|
logininfor.setBrowser(browser);
|
||||||
|
logininfor.setOs(os);
|
||||||
|
logininfor.setMsg(message);
|
||||||
|
// 日志状态
|
||||||
|
if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
|
||||||
|
{
|
||||||
|
logininfor.setStatus(Constants.SUCCESS);
|
||||||
|
}
|
||||||
|
else if (Constants.LOGIN_FAIL.equals(status))
|
||||||
|
{
|
||||||
|
logininfor.setStatus(Constants.FAIL);
|
||||||
|
}
|
||||||
|
// 插入数据
|
||||||
|
SpringUtils.getBean(SysLogininforService.class).insertLogininfor(logininfor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作日志记录
|
||||||
|
*
|
||||||
|
* @param operLog 操作日志信息
|
||||||
|
* @return 任务task
|
||||||
|
*/
|
||||||
|
public static TimerTask recordOper(final SysOperLog operLog)
|
||||||
|
{
|
||||||
|
return new TimerTask()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
// 远程查询操作地点
|
||||||
|
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
|
||||||
|
SpringUtils.getBean(SysOperLogService.class).insertOperlog(operLog);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package net.hzvtc.framework.security.filter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import net.hzvtc.framework.web.service.TokenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token过滤器 验证token有效性
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private TokenService tokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws ServletException, IOException
|
||||||
|
{
|
||||||
|
LoginUser loginUser = tokenService.getLoginUser(request);
|
||||||
|
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
|
||||||
|
{
|
||||||
|
tokenService.verifyToken(loginUser);
|
||||||
|
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
||||||
|
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package net.hzvtc.framework.security.handle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.HttpStatus;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证失败处理类 返回未授权
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = -8970718410437077606L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
int code = HttpStatus.UNAUTHORIZED;
|
||||||
|
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
|
||||||
|
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package net.hzvtc.framework.security.handle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.constant.HttpStatus;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import net.hzvtc.framework.manager.AsyncManager;
|
||||||
|
import net.hzvtc.framework.manager.factory.AsyncFactory;
|
||||||
|
import net.hzvtc.framework.web.service.TokenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义退出处理类 返回成功
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private TokenService tokenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出处理
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException
|
||||||
|
{
|
||||||
|
LoginUser loginUser = tokenService.getLoginUser(request);
|
||||||
|
if (StringUtils.isNotNull(loginUser))
|
||||||
|
{
|
||||||
|
String userName = loginUser.getUsername();
|
||||||
|
// 删除用户缓存记录
|
||||||
|
tokenService.delLoginUser(loginUser.getToken());
|
||||||
|
// 记录用户退出日志
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
|
||||||
|
}
|
||||||
|
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package net.hzvtc.framework.web.domain;
|
||||||
|
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import net.hzvtc.common.utils.Arith;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import net.hzvtc.framework.web.domain.server.Cpu;
|
||||||
|
import net.hzvtc.framework.web.domain.server.Jvm;
|
||||||
|
import net.hzvtc.framework.web.domain.server.Mem;
|
||||||
|
import net.hzvtc.framework.web.domain.server.Sys;
|
||||||
|
import net.hzvtc.framework.web.domain.server.SysFile;
|
||||||
|
import oshi.SystemInfo;
|
||||||
|
import oshi.hardware.CentralProcessor;
|
||||||
|
import oshi.hardware.CentralProcessor.TickType;
|
||||||
|
import oshi.hardware.GlobalMemory;
|
||||||
|
import oshi.hardware.HardwareAbstractionLayer;
|
||||||
|
import oshi.software.os.FileSystem;
|
||||||
|
import oshi.software.os.OSFileStore;
|
||||||
|
import oshi.software.os.OperatingSystem;
|
||||||
|
import oshi.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class Server
|
||||||
|
{
|
||||||
|
private static final int OSHI_WAIT_SECOND = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU相关信息
|
||||||
|
*/
|
||||||
|
private Cpu cpu = new Cpu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 內存相关信息
|
||||||
|
*/
|
||||||
|
private Mem mem = new Mem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JVM相关信息
|
||||||
|
*/
|
||||||
|
private Jvm jvm = new Jvm();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器相关信息
|
||||||
|
*/
|
||||||
|
private Sys sys = new Sys();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 磁盘相关信息
|
||||||
|
*/
|
||||||
|
private List<SysFile> sysFiles = new LinkedList<SysFile>();
|
||||||
|
|
||||||
|
public Cpu getCpu()
|
||||||
|
{
|
||||||
|
return cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCpu(Cpu cpu)
|
||||||
|
{
|
||||||
|
this.cpu = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mem getMem()
|
||||||
|
{
|
||||||
|
return mem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMem(Mem mem)
|
||||||
|
{
|
||||||
|
this.mem = mem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jvm getJvm()
|
||||||
|
{
|
||||||
|
return jvm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJvm(Jvm jvm)
|
||||||
|
{
|
||||||
|
this.jvm = jvm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sys getSys()
|
||||||
|
{
|
||||||
|
return sys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSys(Sys sys)
|
||||||
|
{
|
||||||
|
this.sys = sys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SysFile> getSysFiles()
|
||||||
|
{
|
||||||
|
return sysFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSysFiles(List<SysFile> sysFiles)
|
||||||
|
{
|
||||||
|
this.sysFiles = sysFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyTo() throws Exception
|
||||||
|
{
|
||||||
|
SystemInfo si = new SystemInfo();
|
||||||
|
HardwareAbstractionLayer hal = si.getHardware();
|
||||||
|
|
||||||
|
setCpuInfo(hal.getProcessor());
|
||||||
|
|
||||||
|
setMemInfo(hal.getMemory());
|
||||||
|
|
||||||
|
setSysInfo();
|
||||||
|
|
||||||
|
setJvmInfo();
|
||||||
|
|
||||||
|
setSysFiles(si.getOperatingSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置CPU信息
|
||||||
|
*/
|
||||||
|
private void setCpuInfo(CentralProcessor processor)
|
||||||
|
{
|
||||||
|
// CPU信息
|
||||||
|
long[] prevTicks = processor.getSystemCpuLoadTicks();
|
||||||
|
Util.sleep(OSHI_WAIT_SECOND);
|
||||||
|
long[] ticks = processor.getSystemCpuLoadTicks();
|
||||||
|
long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
|
||||||
|
long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
|
||||||
|
long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
|
||||||
|
long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
|
||||||
|
long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
|
||||||
|
long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
|
||||||
|
long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
|
||||||
|
long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
|
||||||
|
long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
|
||||||
|
cpu.setCpuNum(processor.getLogicalProcessorCount());
|
||||||
|
cpu.setTotal(totalCpu);
|
||||||
|
cpu.setSys(cSys);
|
||||||
|
cpu.setUsed(user);
|
||||||
|
cpu.setWait(iowait);
|
||||||
|
cpu.setFree(idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置内存信息
|
||||||
|
*/
|
||||||
|
private void setMemInfo(GlobalMemory memory)
|
||||||
|
{
|
||||||
|
mem.setTotal(memory.getTotal());
|
||||||
|
mem.setUsed(memory.getTotal() - memory.getAvailable());
|
||||||
|
mem.setFree(memory.getAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置服务器信息
|
||||||
|
*/
|
||||||
|
private void setSysInfo()
|
||||||
|
{
|
||||||
|
Properties props = System.getProperties();
|
||||||
|
sys.setComputerName(IpUtils.getHostName());
|
||||||
|
sys.setComputerIp(IpUtils.getHostIp());
|
||||||
|
sys.setOsName(props.getProperty("os.name"));
|
||||||
|
sys.setOsArch(props.getProperty("os.arch"));
|
||||||
|
sys.setUserDir(props.getProperty("user.dir"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置Java虚拟机
|
||||||
|
*/
|
||||||
|
private void setJvmInfo() throws UnknownHostException
|
||||||
|
{
|
||||||
|
Properties props = System.getProperties();
|
||||||
|
jvm.setTotal(Runtime.getRuntime().totalMemory());
|
||||||
|
jvm.setMax(Runtime.getRuntime().maxMemory());
|
||||||
|
jvm.setFree(Runtime.getRuntime().freeMemory());
|
||||||
|
jvm.setVersion(props.getProperty("java.version"));
|
||||||
|
jvm.setHome(props.getProperty("java.home"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置磁盘信息
|
||||||
|
*/
|
||||||
|
private void setSysFiles(OperatingSystem os)
|
||||||
|
{
|
||||||
|
FileSystem fileSystem = os.getFileSystem();
|
||||||
|
List<OSFileStore> fsArray = fileSystem.getFileStores();
|
||||||
|
for (OSFileStore fs : fsArray)
|
||||||
|
{
|
||||||
|
long free = fs.getUsableSpace();
|
||||||
|
long total = fs.getTotalSpace();
|
||||||
|
long used = total - free;
|
||||||
|
SysFile sysFile = new SysFile();
|
||||||
|
sysFile.setDirName(fs.getMount());
|
||||||
|
sysFile.setSysTypeName(fs.getType());
|
||||||
|
sysFile.setTypeName(fs.getName());
|
||||||
|
sysFile.setTotal(convertFileSize(total));
|
||||||
|
sysFile.setFree(convertFileSize(free));
|
||||||
|
sysFile.setUsed(convertFileSize(used));
|
||||||
|
sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100));
|
||||||
|
sysFiles.add(sysFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字节转换
|
||||||
|
*
|
||||||
|
* @param size 字节大小
|
||||||
|
* @return 转换后值
|
||||||
|
*/
|
||||||
|
public String convertFileSize(long size)
|
||||||
|
{
|
||||||
|
long kb = 1024;
|
||||||
|
long mb = kb * 1024;
|
||||||
|
long gb = mb * 1024;
|
||||||
|
if (size >= gb)
|
||||||
|
{
|
||||||
|
return String.format("%.1f GB", (float) size / gb);
|
||||||
|
}
|
||||||
|
else if (size >= mb)
|
||||||
|
{
|
||||||
|
float f = (float) size / mb;
|
||||||
|
return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
|
||||||
|
}
|
||||||
|
else if (size >= kb)
|
||||||
|
{
|
||||||
|
float f = (float) size / kb;
|
||||||
|
return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return String.format("%d B", size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package net.hzvtc.framework.web.domain.server;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.Arith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class Cpu
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 核心数
|
||||||
|
*/
|
||||||
|
private int cpuNum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU总的使用率
|
||||||
|
*/
|
||||||
|
private double total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU系统使用率
|
||||||
|
*/
|
||||||
|
private double sys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU用户使用率
|
||||||
|
*/
|
||||||
|
private double used;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU当前等待率
|
||||||
|
*/
|
||||||
|
private double wait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU当前空闲率
|
||||||
|
*/
|
||||||
|
private double free;
|
||||||
|
|
||||||
|
public int getCpuNum()
|
||||||
|
{
|
||||||
|
return cpuNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCpuNum(int cpuNum)
|
||||||
|
{
|
||||||
|
this.cpuNum = cpuNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTotal()
|
||||||
|
{
|
||||||
|
return Arith.round(Arith.mul(total, 100), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(double total)
|
||||||
|
{
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getSys()
|
||||||
|
{
|
||||||
|
return Arith.round(Arith.mul(sys / total, 100), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSys(double sys)
|
||||||
|
{
|
||||||
|
this.sys = sys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsed()
|
||||||
|
{
|
||||||
|
return Arith.round(Arith.mul(used / total, 100), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsed(double used)
|
||||||
|
{
|
||||||
|
this.used = used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWait()
|
||||||
|
{
|
||||||
|
return Arith.round(Arith.mul(wait / total, 100), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWait(double wait)
|
||||||
|
{
|
||||||
|
this.wait = wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFree()
|
||||||
|
{
|
||||||
|
return Arith.round(Arith.mul(free / total, 100), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFree(double free)
|
||||||
|
{
|
||||||
|
this.free = free;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package net.hzvtc.framework.web.domain.server;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import net.hzvtc.common.utils.Arith;
|
||||||
|
import net.hzvtc.common.utils.DateUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JVM相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class Jvm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 当前JVM占用的内存总数(M)
|
||||||
|
*/
|
||||||
|
private double total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JVM最大可用内存总数(M)
|
||||||
|
*/
|
||||||
|
private double max;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JVM空闲内存(M)
|
||||||
|
*/
|
||||||
|
private double free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDK版本
|
||||||
|
*/
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDK路径
|
||||||
|
*/
|
||||||
|
private String home;
|
||||||
|
|
||||||
|
public double getTotal()
|
||||||
|
{
|
||||||
|
return Arith.div(total, (1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(double total)
|
||||||
|
{
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getMax()
|
||||||
|
{
|
||||||
|
return Arith.div(max, (1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMax(double max)
|
||||||
|
{
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFree()
|
||||||
|
{
|
||||||
|
return Arith.div(free, (1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFree(double free)
|
||||||
|
{
|
||||||
|
this.free = free;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsed()
|
||||||
|
{
|
||||||
|
return Arith.div(total - free, (1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsage()
|
||||||
|
{
|
||||||
|
return Arith.mul(Arith.div(total - free, total, 4), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取JDK名称
|
||||||
|
*/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return ManagementFactory.getRuntimeMXBean().getVmName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVersion()
|
||||||
|
{
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(String version)
|
||||||
|
{
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHome()
|
||||||
|
{
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHome(String home)
|
||||||
|
{
|
||||||
|
this.home = home;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDK启动时间
|
||||||
|
*/
|
||||||
|
public String getStartTime()
|
||||||
|
{
|
||||||
|
return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDK运行时间
|
||||||
|
*/
|
||||||
|
public String getRunTime()
|
||||||
|
{
|
||||||
|
return DateUtils.getDatePoor(DateUtils.getNowDate(), DateUtils.getServerStartDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package net.hzvtc.framework.web.domain.server;
|
||||||
|
|
||||||
|
import net.hzvtc.common.utils.Arith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 內存相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class Mem
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 内存总量
|
||||||
|
*/
|
||||||
|
private double total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已用内存
|
||||||
|
*/
|
||||||
|
private double used;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余内存
|
||||||
|
*/
|
||||||
|
private double free;
|
||||||
|
|
||||||
|
public double getTotal()
|
||||||
|
{
|
||||||
|
return Arith.div(total, (1024 * 1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(long total)
|
||||||
|
{
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsed()
|
||||||
|
{
|
||||||
|
return Arith.div(used, (1024 * 1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsed(long used)
|
||||||
|
{
|
||||||
|
this.used = used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getFree()
|
||||||
|
{
|
||||||
|
return Arith.div(free, (1024 * 1024 * 1024), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFree(long free)
|
||||||
|
{
|
||||||
|
this.free = free;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsage()
|
||||||
|
{
|
||||||
|
return Arith.mul(Arith.div(used, total, 4), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package net.hzvtc.framework.web.domain.server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class Sys
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 服务器名称
|
||||||
|
*/
|
||||||
|
private String computerName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器Ip
|
||||||
|
*/
|
||||||
|
private String computerIp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目路径
|
||||||
|
*/
|
||||||
|
private String userDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作系统
|
||||||
|
*/
|
||||||
|
private String osName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统架构
|
||||||
|
*/
|
||||||
|
private String osArch;
|
||||||
|
|
||||||
|
public String getComputerName()
|
||||||
|
{
|
||||||
|
return computerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComputerName(String computerName)
|
||||||
|
{
|
||||||
|
this.computerName = computerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getComputerIp()
|
||||||
|
{
|
||||||
|
return computerIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComputerIp(String computerIp)
|
||||||
|
{
|
||||||
|
this.computerIp = computerIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserDir()
|
||||||
|
{
|
||||||
|
return userDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserDir(String userDir)
|
||||||
|
{
|
||||||
|
this.userDir = userDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOsName()
|
||||||
|
{
|
||||||
|
return osName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOsName(String osName)
|
||||||
|
{
|
||||||
|
this.osName = osName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOsArch()
|
||||||
|
{
|
||||||
|
return osArch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOsArch(String osArch)
|
||||||
|
{
|
||||||
|
this.osArch = osArch;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package net.hzvtc.framework.web.domain.server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统文件相关信息
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
public class SysFile
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 盘符路径
|
||||||
|
*/
|
||||||
|
private String dirName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 盘符类型
|
||||||
|
*/
|
||||||
|
private String sysTypeName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件类型
|
||||||
|
*/
|
||||||
|
private String typeName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总大小
|
||||||
|
*/
|
||||||
|
private String total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 剩余大小
|
||||||
|
*/
|
||||||
|
private String free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已经使用量
|
||||||
|
*/
|
||||||
|
private String used;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源的使用率
|
||||||
|
*/
|
||||||
|
private double usage;
|
||||||
|
|
||||||
|
public String getDirName()
|
||||||
|
{
|
||||||
|
return dirName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDirName(String dirName)
|
||||||
|
{
|
||||||
|
this.dirName = dirName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSysTypeName()
|
||||||
|
{
|
||||||
|
return sysTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSysTypeName(String sysTypeName)
|
||||||
|
{
|
||||||
|
this.sysTypeName = sysTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTypeName()
|
||||||
|
{
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTypeName(String typeName)
|
||||||
|
{
|
||||||
|
this.typeName = typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTotal()
|
||||||
|
{
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(String total)
|
||||||
|
{
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFree()
|
||||||
|
{
|
||||||
|
return free;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFree(String free)
|
||||||
|
{
|
||||||
|
this.free = free;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsed()
|
||||||
|
{
|
||||||
|
return used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsed(String used)
|
||||||
|
{
|
||||||
|
this.used = used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getUsage()
|
||||||
|
{
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsage(double usage)
|
||||||
|
{
|
||||||
|
this.usage = usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
package net.hzvtc.framework.web.exception;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.HttpStatus;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.exception.DemoModeException;
|
||||||
|
import net.hzvtc.common.exception.ServiceException;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局异常处理器
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限校验异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
|
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
|
||||||
|
return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求方式不支持
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||||
|
public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(ServiceException.class)
|
||||||
|
public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
Integer code = e.getCode();
|
||||||
|
return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截未知的运行时异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求地址'{}',发生未知异常.", requestURI, e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public AjaxResult handleException(Exception e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求地址'{}',发生系统异常.", requestURI, e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义验证异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BindException.class)
|
||||||
|
public AjaxResult handleBindException(BindException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
String message = e.getAllErrors().get(0).getDefaultMessage();
|
||||||
|
return AjaxResult.error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义验证异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
String message = e.getBindingResult().getFieldError().getDefaultMessage();
|
||||||
|
return AjaxResult.error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 演示模式异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(DemoModeException.class)
|
||||||
|
public AjaxResult handleDemoModeException(DemoModeException e) {
|
||||||
|
return AjaxResult.error("演示模式,不允许操作");
|
||||||
|
}
|
||||||
|
}
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysRole;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首创 自定义权限实现,ss取自SpringSecurity首字母
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Service("ss")
|
||||||
|
public class PermissionService
|
||||||
|
{
|
||||||
|
/** 所有权限标识 */
|
||||||
|
private static final String ALL_PERMISSION = "*:*:*";
|
||||||
|
|
||||||
|
/** 管理员角色权限标识 */
|
||||||
|
private static final String SUPER_ADMIN = "admin";
|
||||||
|
|
||||||
|
private static final String ROLE_DELIMETER = ",";
|
||||||
|
|
||||||
|
private static final String PERMISSION_DELIMETER = ",";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户是否具备某权限
|
||||||
|
*
|
||||||
|
* @param permission 权限字符串
|
||||||
|
* @return 用户是否具备某权限
|
||||||
|
*/
|
||||||
|
public boolean hasPermi(String permission)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(permission))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return hasPermissions(loginUser.getPermissions(), permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户是否不具备某权限,与 hasPermi逻辑相反
|
||||||
|
*
|
||||||
|
* @param permission 权限字符串
|
||||||
|
* @return 用户是否不具备某权限
|
||||||
|
*/
|
||||||
|
public boolean lacksPermi(String permission)
|
||||||
|
{
|
||||||
|
return hasPermi(permission) != true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户是否具有以下任意一个权限
|
||||||
|
*
|
||||||
|
* @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
|
||||||
|
* @return 用户是否具有以下任意一个权限
|
||||||
|
*/
|
||||||
|
public boolean hasAnyPermi(String permissions)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(permissions))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Set<String> authorities = loginUser.getPermissions();
|
||||||
|
for (String permission : permissions.split(PERMISSION_DELIMETER))
|
||||||
|
{
|
||||||
|
if (permission != null && hasPermissions(authorities, permission))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断用户是否拥有某个角色
|
||||||
|
*
|
||||||
|
* @param role 角色字符串
|
||||||
|
* @return 用户是否具备某角色
|
||||||
|
*/
|
||||||
|
public boolean hasRole(String role)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(role))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (SysRole sysRole : loginUser.getUser().getRoles())
|
||||||
|
{
|
||||||
|
String roleKey = sysRole.getRoleKey();
|
||||||
|
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户是否不具备某角色,与 isRole逻辑相反。
|
||||||
|
*
|
||||||
|
* @param role 角色名称
|
||||||
|
* @return 用户是否不具备某角色
|
||||||
|
*/
|
||||||
|
public boolean lacksRole(String role)
|
||||||
|
{
|
||||||
|
return hasRole(role) != true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户是否具有以下任意一个角色
|
||||||
|
*
|
||||||
|
* @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
|
||||||
|
* @return 用户是否具有以下任意一个角色
|
||||||
|
*/
|
||||||
|
public boolean hasAnyRoles(String roles)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(roles))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String role : roles.split(ROLE_DELIMETER))
|
||||||
|
{
|
||||||
|
if (hasRole(role))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否包含权限
|
||||||
|
*
|
||||||
|
* @param permissions 权限列表
|
||||||
|
* @param permission 权限字符串
|
||||||
|
* @return 用户是否具备某权限
|
||||||
|
*/
|
||||||
|
private boolean hasPermissions(Set<String> permissions, String permission)
|
||||||
|
{
|
||||||
|
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
|
||||||
|
}
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.core.domain.AjaxResult;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysUser;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.core.redis.RedisCache;
|
||||||
|
import net.hzvtc.common.exception.ServiceException;
|
||||||
|
import net.hzvtc.common.exception.user.CaptchaException;
|
||||||
|
import net.hzvtc.common.exception.user.CaptchaExpireException;
|
||||||
|
import net.hzvtc.common.exception.user.UserPasswordNotMatchException;
|
||||||
|
import net.hzvtc.common.utils.DateUtils;
|
||||||
|
import net.hzvtc.common.utils.MessageUtils;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import net.hzvtc.email.service.EmailService;
|
||||||
|
import net.hzvtc.framework.manager.AsyncManager;
|
||||||
|
import net.hzvtc.framework.manager.factory.AsyncFactory;
|
||||||
|
import net.hzvtc.system.service.SysConfigService;
|
||||||
|
import net.hzvtc.system.service.SysUserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录校验方法
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SysLoginService {
|
||||||
|
@Autowired
|
||||||
|
private TokenService tokenService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EmailService emailService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录验证
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param password 密码
|
||||||
|
* @param code 验证码
|
||||||
|
* @param uuid 唯一标识
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public String login(String username, String password, String code, String uuid, int loginType) {
|
||||||
|
//sso 指定用户登录
|
||||||
|
// if (loginType == 3) {
|
||||||
|
// username = "ssoLoginUser";
|
||||||
|
// password = "ssoLoginUser_123";
|
||||||
|
// } else {
|
||||||
|
// boolean captchaOnOff = configService.selectCaptchaOnOff();
|
||||||
|
// // 验证码开关
|
||||||
|
// if (captchaOnOff && loginType == 0) {
|
||||||
|
// validateCaptcha(username, code, uuid);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
boolean captchaOnOff = configService.selectCaptchaOnOff();
|
||||||
|
// 验证码开关
|
||||||
|
if (captchaOnOff && (loginType == 0 || loginType == 2)) {
|
||||||
|
validateCaptcha(username, code, uuid);
|
||||||
|
}
|
||||||
|
// 用户验证
|
||||||
|
Authentication authentication = null;
|
||||||
|
try {
|
||||||
|
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
|
||||||
|
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (e instanceof BadCredentialsException) {
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
|
||||||
|
throw new UserPasswordNotMatchException();
|
||||||
|
} else {
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
|
||||||
|
throw new ServiceException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
|
||||||
|
// if (loginType == 0) {
|
||||||
|
// if (loginUser.getPortalLogin() != 1) {
|
||||||
|
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("no.portal.login")));
|
||||||
|
// throw new UserNoLoginPermissionException();
|
||||||
|
// }
|
||||||
|
// } else if (loginType == 1) {
|
||||||
|
// if (loginUser.getWebLogin() != 1) {
|
||||||
|
// AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("no.web.login")));
|
||||||
|
// throw new UserNoLoginPermissionException();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
|
||||||
|
|
||||||
|
recordLoginInfo(loginUser.getUserId());
|
||||||
|
// 生成token
|
||||||
|
return tokenService.createToken(loginUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param code 验证码
|
||||||
|
* @param uuid 唯一标识
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public void validateCaptcha(String username, String code, String uuid) {
|
||||||
|
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
|
||||||
|
String captcha = redisCache.getCacheObject(verifyKey);
|
||||||
|
redisCache.deleteObject(verifyKey);
|
||||||
|
if (captcha == null) {
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
|
||||||
|
throw new CaptchaExpireException();
|
||||||
|
}
|
||||||
|
if (!code.equalsIgnoreCase(captcha)) {
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
|
||||||
|
throw new CaptchaException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录登录信息
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
public void recordLoginInfo(Long userId) {
|
||||||
|
SysUser sysUser = new SysUser();
|
||||||
|
sysUser.setUserId(userId);
|
||||||
|
sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
|
||||||
|
sysUser.setLoginDate(DateUtils.getNowDate());
|
||||||
|
userService.updateUserProfile(sysUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AjaxResult resetPasswordByName(String loginId) {
|
||||||
|
SysUser user = userService.selectUserByUserName(loginId);
|
||||||
|
if (user == null) {
|
||||||
|
return AjaxResult.error(500, "帐号不存在");
|
||||||
|
} else if (user.getEmail() == null || user.getEmail().isEmpty()) {
|
||||||
|
return AjaxResult.error(500, "此帐号电子邮箱为空");
|
||||||
|
}
|
||||||
|
String passwd = genRandomNum();
|
||||||
|
userService.resetUserPwd(loginId, SecurityUtils.encryptPassword(passwd));
|
||||||
|
emailService.sendSimpleEmail("密码重置", "你的密码已经重置为:" + passwd + ",请及时修改!", user.getEmail());
|
||||||
|
return AjaxResult.success("密码已重置!");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String genRandomNum() {
|
||||||
|
int maxNum = 36;
|
||||||
|
int i;
|
||||||
|
int count = 0;
|
||||||
|
char[] str = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
|
||||||
|
'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
|
||||||
|
'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
|
||||||
|
StringBuffer pwd = new StringBuffer("");
|
||||||
|
Random r = new Random();
|
||||||
|
while (count < 8) {
|
||||||
|
i = Math.abs(r.nextInt(maxNum));
|
||||||
|
if (i >= 0 && i < str.length) {
|
||||||
|
pwd.append(str[i]);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pwd.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysRole;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysUser;
|
||||||
|
import net.hzvtc.system.service.SysMenuService;
|
||||||
|
import net.hzvtc.system.service.SysRoleService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户权限处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SysPermissionService {
|
||||||
|
@Autowired
|
||||||
|
private SysRoleService roleService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysMenuService menuService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色数据权限
|
||||||
|
*
|
||||||
|
* @param user 用户信息
|
||||||
|
* @return 角色权限信息
|
||||||
|
*/
|
||||||
|
public Set<String> getRolePermission(SysUser user) {
|
||||||
|
Set<String> roles = new HashSet<String>();
|
||||||
|
// 管理员拥有所有权限
|
||||||
|
if (user.isAdmin()) {
|
||||||
|
roles.add("admin");
|
||||||
|
} else {
|
||||||
|
SysRole sysRole = new SysRole();
|
||||||
|
sysRole.setUserId(user.getUserId());
|
||||||
|
sysRole.setSysId(user.getSysId());
|
||||||
|
roles.addAll(roleService.selectRolePermissionByUserId(sysRole));
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取菜单数据权限
|
||||||
|
*
|
||||||
|
* @param user 用户信息
|
||||||
|
* @return 菜单权限信息
|
||||||
|
*/
|
||||||
|
public Set<String> getMenuPermission(SysUser user) {
|
||||||
|
Set<String> perms = new HashSet<String>();
|
||||||
|
// 管理员拥有所有权限
|
||||||
|
if (user.isAdmin()) {
|
||||||
|
perms.add("*:*:*");
|
||||||
|
} else {
|
||||||
|
perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
|
||||||
|
}
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.constant.UserConstants;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysUser;
|
||||||
|
import net.hzvtc.common.core.domain.model.RegisterBody;
|
||||||
|
import net.hzvtc.common.core.redis.RedisCache;
|
||||||
|
import net.hzvtc.common.exception.user.CaptchaException;
|
||||||
|
import net.hzvtc.common.exception.user.CaptchaExpireException;
|
||||||
|
import net.hzvtc.common.utils.MessageUtils;
|
||||||
|
import net.hzvtc.common.utils.SecurityUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.framework.manager.AsyncManager;
|
||||||
|
import net.hzvtc.framework.manager.factory.AsyncFactory;
|
||||||
|
import net.hzvtc.system.service.SysConfigService;
|
||||||
|
import net.hzvtc.system.service.SysUserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册校验方法
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SysRegisterService {
|
||||||
|
@Autowired
|
||||||
|
private SysUserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
*/
|
||||||
|
public String register(RegisterBody registerBody) {
|
||||||
|
String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword();
|
||||||
|
|
||||||
|
boolean captchaOnOff = configService.selectCaptchaOnOff();
|
||||||
|
// 验证码开关
|
||||||
|
if (captchaOnOff) {
|
||||||
|
validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(username)) {
|
||||||
|
msg = "用户名不能为空";
|
||||||
|
} else if (StringUtils.isEmpty(password)) {
|
||||||
|
msg = "用户密码不能为空";
|
||||||
|
} else if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|
||||||
|
|| username.length() > UserConstants.USERNAME_MAX_LENGTH) {
|
||||||
|
msg = "账户长度必须在2到20个字符之间";
|
||||||
|
} else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|
||||||
|
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
|
||||||
|
msg = "密码长度必须在5到20个字符之间";
|
||||||
|
} else if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(username))) {
|
||||||
|
msg = "保存用户'" + username + "'失败,注册账号已存在";
|
||||||
|
} else {
|
||||||
|
SysUser sysUser = new SysUser();
|
||||||
|
sysUser.setUserName(username);
|
||||||
|
sysUser.setNickName(username);
|
||||||
|
sysUser.setPassword(SecurityUtils.encryptPassword(registerBody.getPassword()));
|
||||||
|
boolean regFlag = userService.registerUser(sysUser);
|
||||||
|
if (!regFlag) {
|
||||||
|
msg = "注册失败,请联系系统管理人员";
|
||||||
|
} else {
|
||||||
|
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER,
|
||||||
|
MessageUtils.message("user.register.success")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param code 验证码
|
||||||
|
* @param uuid 唯一标识
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public void validateCaptcha(String username, String code, String uuid) {
|
||||||
|
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
|
||||||
|
String captcha = redisCache.getCacheObject(verifyKey);
|
||||||
|
redisCache.deleteObject(verifyKey);
|
||||||
|
if (captcha == null) {
|
||||||
|
throw new CaptchaExpireException();
|
||||||
|
}
|
||||||
|
if (!code.equalsIgnoreCase(captcha)) {
|
||||||
|
throw new CaptchaException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import eu.bitwalker.useragentutils.UserAgent;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import net.hzvtc.common.constant.Constants;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.core.redis.RedisCache;
|
||||||
|
import net.hzvtc.common.utils.ServletUtils;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.AddressUtils;
|
||||||
|
import net.hzvtc.common.utils.ip.IpUtils;
|
||||||
|
import net.hzvtc.common.utils.uuid.IdUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* token验证处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class TokenService {
|
||||||
|
protected static final long MILLIS_SECOND = 1000;
|
||||||
|
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
|
||||||
|
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
|
||||||
|
// 令牌自定义标识
|
||||||
|
@Value("${token.header}")
|
||||||
|
private String header;
|
||||||
|
// 令牌秘钥
|
||||||
|
@Value("${token.secret}")
|
||||||
|
private String secret;
|
||||||
|
// 令牌有效期(默认30分钟)
|
||||||
|
@Value("${token.expireTime}")
|
||||||
|
private int expireTime;
|
||||||
|
@Autowired
|
||||||
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户身份信息
|
||||||
|
*
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
public LoginUser getLoginUser(HttpServletRequest request) {
|
||||||
|
// 获取请求携带的令牌
|
||||||
|
String token = getToken(request);
|
||||||
|
if (StringUtils.isNotEmpty(token)) {
|
||||||
|
try {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
// 解析对应的权限以及用户信息
|
||||||
|
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||||
|
String userKey = getTokenKey(uuid);
|
||||||
|
LoginUser user = redisCache.getCacheObject(userKey);
|
||||||
|
return user;
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户身份信息
|
||||||
|
*/
|
||||||
|
public void setLoginUser(LoginUser loginUser) {
|
||||||
|
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken())) {
|
||||||
|
refreshToken(loginUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户身份信息
|
||||||
|
*/
|
||||||
|
public void delLoginUser(String token) {
|
||||||
|
if (StringUtils.isNotEmpty(token)) {
|
||||||
|
String userKey = getTokenKey(token);
|
||||||
|
redisCache.deleteObject(userKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建令牌
|
||||||
|
*
|
||||||
|
* @param loginUser 用户信息
|
||||||
|
* @return 令牌
|
||||||
|
*/
|
||||||
|
public String createToken(LoginUser loginUser) {
|
||||||
|
String token = IdUtils.fastUUID();
|
||||||
|
loginUser.setToken(token);
|
||||||
|
setUserAgent(loginUser);
|
||||||
|
refreshToken(loginUser);
|
||||||
|
|
||||||
|
Map<String, Object> claims = new HashMap<>();
|
||||||
|
claims.put(Constants.LOGIN_USER_KEY, token);
|
||||||
|
return createToken(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
|
||||||
|
*
|
||||||
|
* @param loginUser
|
||||||
|
* @return 令牌
|
||||||
|
*/
|
||||||
|
public void verifyToken(LoginUser loginUser) {
|
||||||
|
long expireTime = loginUser.getExpireTime();
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
|
||||||
|
refreshToken(loginUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌有效期
|
||||||
|
*
|
||||||
|
* @param loginUser 登录信息
|
||||||
|
*/
|
||||||
|
public void refreshToken(LoginUser loginUser) {
|
||||||
|
loginUser.setLoginTime(System.currentTimeMillis());
|
||||||
|
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
|
||||||
|
// 根据uuid将loginUser缓存
|
||||||
|
String userKey = getTokenKey(loginUser.getToken());
|
||||||
|
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户代理信息
|
||||||
|
*
|
||||||
|
* @param loginUser 登录信息
|
||||||
|
*/
|
||||||
|
public void setUserAgent(LoginUser loginUser) {
|
||||||
|
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||||
|
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
||||||
|
loginUser.setIpaddr(ip);
|
||||||
|
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||||
|
loginUser.setBrowser(userAgent.getBrowser().getName());
|
||||||
|
loginUser.setOs(userAgent.getOperatingSystem().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据声明生成令牌
|
||||||
|
*
|
||||||
|
* @param claims 数据声明
|
||||||
|
* @return 令牌
|
||||||
|
*/
|
||||||
|
private String createToken(Map<String, Object> claims) {
|
||||||
|
// String token = Jwts.builder()
|
||||||
|
// .setClaims(claims)
|
||||||
|
// .signWith(SignatureAlgorithm.HS512, secret).compact();
|
||||||
|
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
|
String token = Jwts.builder().claims(claims).signWith(key, Jwts.SIG.HS512).compact();
|
||||||
|
// signWith(SignatureAlgorithm.HS512, secret).compact();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从令牌中获取数据声明
|
||||||
|
*
|
||||||
|
* @param token 令牌
|
||||||
|
* @return 数据声明
|
||||||
|
*/
|
||||||
|
private Claims parseToken(String token) {
|
||||||
|
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(key)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token).getPayload();
|
||||||
|
// return Jwts.parser()
|
||||||
|
// .setSigningKey(secret)
|
||||||
|
// .parseClaimsJws(token)
|
||||||
|
// .getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从令牌中获取用户名
|
||||||
|
*
|
||||||
|
* @param token 令牌
|
||||||
|
* @return 用户名
|
||||||
|
*/
|
||||||
|
public String getUsernameFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求token
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @return token
|
||||||
|
*/
|
||||||
|
private String getToken(HttpServletRequest request) {
|
||||||
|
String token = request.getHeader(header);
|
||||||
|
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
|
||||||
|
token = token.replace(Constants.TOKEN_PREFIX, "");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTokenKey(String uuid) {
|
||||||
|
return Constants.LOGIN_TOKEN_KEY + uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package net.hzvtc.framework.web.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import net.hzvtc.area.service.ProtectedAreaService;
|
||||||
|
import net.hzvtc.common.core.domain.entity.SysUser;
|
||||||
|
import net.hzvtc.common.core.domain.model.LoginUser;
|
||||||
|
import net.hzvtc.common.enums.UserStatus;
|
||||||
|
import net.hzvtc.common.exception.ServiceException;
|
||||||
|
import net.hzvtc.common.utils.StringUtils;
|
||||||
|
import net.hzvtc.device.service.DeviceInfoService;
|
||||||
|
import net.hzvtc.system.service.SysRoleService;
|
||||||
|
import net.hzvtc.system.service.SysUserService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户验证处理
|
||||||
|
*
|
||||||
|
* @author admin
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysPermissionService permissionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProtectedAreaService protectedAreaService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DeviceInfoService deviceInfoService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
SysUser user = userService.selectUserByUserName(username);
|
||||||
|
if (StringUtils.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", username);
|
||||||
|
throw new ServiceException("登录用户:" + username + " 不存在");
|
||||||
|
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
|
||||||
|
log.info("登录用户:{} 已被删除.", username);
|
||||||
|
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", username);
|
||||||
|
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
|
||||||
|
}
|
||||||
|
return createLoginUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserDetails createLoginUser(SysUser user) {
|
||||||
|
// List<JSONObject> areaList = protectedAreaService.getAreaListByUserId(user.getUserId());
|
||||||
|
// Long userId = null;
|
||||||
|
// if(user.getDeptId()==1L){
|
||||||
|
// userId = 0L;
|
||||||
|
// }else{
|
||||||
|
// userId = user.getUserId();
|
||||||
|
// }
|
||||||
|
// List<JSONObject> deviceList = deviceInfoService.getDeviceInfoListByUserId(userId);
|
||||||
|
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user),
|
||||||
|
user.getWebLogin(), user.getPortalLogin());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user