diff --git a/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java b/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java index 8880dc1b..bf9aa37f 100644 --- a/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java @@ -38,6 +38,9 @@ import com.njcn.gather.monitor.pojo.po.PqMonitor; import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel; import com.njcn.gather.monitor.service.IPqMonitorService; import com.njcn.gather.plan.pojo.enums.DataSourceEnum; +import com.njcn.gather.plan.mapper.AdPlanMapper; +import com.njcn.gather.plan.pojo.po.AdPlan; +import com.njcn.gather.plan.service.IPqDevCheckHistoryService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.storage.service.DetectionDataDealService; import com.njcn.gather.system.cfg.pojo.enums.SceneEnum; @@ -87,6 +90,8 @@ public class PqDevServiceImpl extends ServiceImpl implements private final IDictTypeService dictTypeService; private final ISysUserService userService; private final IPqDevSubService pqDevSubService; + private final AdPlanMapper adPlanMapper; + private final IPqDevCheckHistoryService pqDevCheckHistoryService; @Override public Page listPqDevs(PqDevParam.QueryParam queryParam) { @@ -526,6 +531,7 @@ public class PqDevServiceImpl extends ServiceImpl implements wrapper.set(PqDevSub::getRecheckNum, i) .set(PqDevSub::getCheckState, checkState); pqDevSubService.update(wrapper); + saveCheckHistory(pqDevVo, i, checkState, userId); PqDevParam.QueryParam param = new PqDevParam.QueryParam(); param.setPlanIdList(Arrays.asList(pqDevVo.getPlanId())); @@ -591,6 +597,7 @@ public class PqDevServiceImpl extends ServiceImpl implements w.set(PqDevSub::getReportState, DevReportStateEnum.NOT_GENERATED.getValue()); } w.update(); + saveCheckHistory(devId, userId); PqDevParam.QueryParam param = new PqDevParam.QueryParam(); String planId = dev.getPlanId(); @@ -626,6 +633,37 @@ public class PqDevServiceImpl extends ServiceImpl implements } } + private void saveCheckHistory(PqDevVO pqDevVo, Integer recheckNum, Integer checkState, String userId) { + if (ObjectUtil.isNull(pqDevVo) + || (!CheckStateEnum.CHECKED.getValue().equals(checkState) && !CheckStateEnum.DOCUMENTED.getValue().equals(checkState))) { + return; + } + AdPlan plan = adPlanMapper.selectById(pqDevVo.getPlanId()); + if (ObjectUtil.isNull(plan)) { + return; + } + pqDevVo.setRecheckNum(recheckNum); + pqDevVo.setCheckBy(userId); + pqDevVo.setCheckTime(LocalDateTime.now()); + pqDevCheckHistoryService.saveOrUpdateDeviceHistory(plan, pqDevVo); + } + + private void saveCheckHistory(String devId, String userId) { + PqDevVO pqDevVo = this.baseMapper.selectByDevId(devId); + if (ObjectUtil.isNull(pqDevVo) + || (!CheckStateEnum.CHECKED.getValue().equals(pqDevVo.getCheckState()) && !CheckStateEnum.DOCUMENTED.getValue().equals(pqDevVo.getCheckState()))) { + return; + } + AdPlan plan = adPlanMapper.selectById(pqDevVo.getPlanId()); + if (ObjectUtil.isNull(plan)) { + return; + } + if (StrUtil.isBlank(pqDevVo.getCheckBy())) { + pqDevVo.setCheckBy(userId); + } + pqDevCheckHistoryService.saveOrUpdateDeviceHistory(plan, pqDevVo); + } + @Override public void updatePqDevReportState(String devId, int reportState) { LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); diff --git a/detection/src/main/java/com/njcn/gather/plan/controller/AdPlanController.java b/detection/src/main/java/com/njcn/gather/plan/controller/AdPlanController.java index 03239003..1fe71fcb 100644 --- a/detection/src/main/java/com/njcn/gather/plan/controller/AdPlanController.java +++ b/detection/src/main/java/com/njcn/gather/plan/controller/AdPlanController.java @@ -206,7 +206,7 @@ public class AdPlanController extends BaseController { public HttpResult statistics(@RequestBody @Validated AdPlanParam.StatisticsParam param) { String methodDescribe = getMethodDescribe("statistics"); LogUtil.njcnDebug(log, "{},查询数据为:{}", methodDescribe, param); - PlanStatisticsVO result = adPlanService.statistics(param.getPlanId()); + PlanStatisticsVO result = adPlanService.statistics(param); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); } diff --git a/detection/src/main/java/com/njcn/gather/plan/mapper/PqDevCheckHistoryMapper.java b/detection/src/main/java/com/njcn/gather/plan/mapper/PqDevCheckHistoryMapper.java new file mode 100644 index 00000000..1f03387f --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/plan/mapper/PqDevCheckHistoryMapper.java @@ -0,0 +1,15 @@ +package com.njcn.gather.plan.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.plan.pojo.po.PqDevCheckHistory; +import com.njcn.gather.plan.pojo.vo.PlanStatisticsItemVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface PqDevCheckHistoryMapper extends MPJBaseMapper { + + List listItemDistributions(@Param("planId") String planId, + @Param("manufacturer") String manufacturer, + @Param("devType") String devType); +} diff --git a/detection/src/main/java/com/njcn/gather/plan/mapper/mapping/PqDevCheckHistoryMapper.xml b/detection/src/main/java/com/njcn/gather/plan/mapper/mapping/PqDevCheckHistoryMapper.xml new file mode 100644 index 00000000..66174457 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/plan/mapper/mapping/PqDevCheckHistoryMapper.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/detection/src/main/java/com/njcn/gather/plan/pojo/param/AdPlanParam.java b/detection/src/main/java/com/njcn/gather/plan/pojo/param/AdPlanParam.java index dab28f30..75a5d064 100644 --- a/detection/src/main/java/com/njcn/gather/plan/pojo/param/AdPlanParam.java +++ b/detection/src/main/java/com/njcn/gather/plan/pojo/param/AdPlanParam.java @@ -136,5 +136,11 @@ public class AdPlanParam { @ApiModelProperty(value = "检测计划ID", required = true) @NotBlank(message = DetectionValidMessage.PLAN_ID_NOT_BLANK) private String planId; + + @ApiModelProperty("设备厂家") + private String manufacturer; + + @ApiModelProperty("设备类型") + private String devType; } } diff --git a/detection/src/main/java/com/njcn/gather/plan/pojo/po/PqDevCheckHistory.java b/detection/src/main/java/com/njcn/gather/plan/pojo/po/PqDevCheckHistory.java new file mode 100644 index 00000000..909241f5 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/plan/pojo/po/PqDevCheckHistory.java @@ -0,0 +1,60 @@ +package com.njcn.gather.plan.pojo.po; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("pq_dev_check_history") +public class PqDevCheckHistory { + + @TableId(value = "Id", type = IdType.ASSIGN_UUID) + private String id; + + @TableField("Plan_Id") + private String planId; + + @TableField("Dev_Id") + private String devId; + + @TableField("Dev_Type") + private String devType; + + @TableField("Manufacturer") + private String manufacturer; + + @TableField("ReCheck_Num") + private Integer recheckNum; + + @TableField("Item_Id") + private String itemId; + + @TableField("Item_Name") + private String itemName; + + @TableField("Result") + private Integer result; + + @TableField("Check_Time") + private LocalDateTime checkTime; + + @TableField("State") + private Integer state; + + @TableField("Create_By") + private String createBy; + + @TableField(value = "Create_Time", fill = FieldFill.INSERT) + private LocalDateTime createTime; + + @TableField("Update_By") + private String updateBy; + + @TableField(value = "Update_Time", fill = FieldFill.UPDATE) + private LocalDateTime updateTime; +} diff --git a/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsItemVO.java b/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsItemVO.java index 4c455527..245bd956 100644 --- a/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsItemVO.java +++ b/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsItemVO.java @@ -2,41 +2,24 @@ package com.njcn.gather.plan.pojo.vo; import lombok.Data; -import java.math.BigDecimal; - /** - * 检测计划单个检测项统计结果。 + * 检测计划单个检测大项柱状图统计结果。 */ @Data public class PlanStatisticsItemVO { /** - * 检测项ID。 + * 检测大项ID。 */ private String itemId; /** - * 检测项名称。 + * 检测大项名称。 */ private String itemName; - /** - * 执行次数。 - */ - private Integer totalCount; - - /** - * 合格次数。 - */ - private Integer qualifiedCount; - /** * 不合格次数。 */ private Integer unqualifiedCount; - - /** - * 合格率,百分制。 - */ - private BigDecimal passRate; } diff --git a/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVO.java b/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVO.java index 018be326..0ca1d56a 100644 --- a/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVO.java +++ b/detection/src/main/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVO.java @@ -31,6 +31,11 @@ public class PlanStatisticsVO { */ private Integer checkedDeviceCount; + /** + * 未检设备总数。 + */ + private Integer uncheckedDeviceCount; + /** * 第一次检测合格的设备数量。 */ @@ -46,6 +51,11 @@ public class PlanStatisticsVO { */ private Integer thirdOrMoreQualifiedDeviceCount; + /** + * 最终合格的设备数量。 + */ + private Integer qualifiedDeviceCount; + /** * 最终不合格的设备数量。 */ diff --git a/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java b/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java index 574d87aa..95b7b2b7 100644 --- a/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java +++ b/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java @@ -112,7 +112,7 @@ public interface IAdPlanService extends IService { * @param planId 检测计划ID * @return 检测计划统计结果 */ - PlanStatisticsVO statistics(String planId); + PlanStatisticsVO statistics(AdPlanParam.StatisticsParam param); /** * 导出检测计划数据 diff --git a/detection/src/main/java/com/njcn/gather/plan/service/IPqDevCheckHistoryService.java b/detection/src/main/java/com/njcn/gather/plan/service/IPqDevCheckHistoryService.java new file mode 100644 index 00000000..104035a6 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/plan/service/IPqDevCheckHistoryService.java @@ -0,0 +1,18 @@ +package com.njcn.gather.plan.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.device.pojo.vo.PqDevVO; +import com.njcn.gather.plan.pojo.po.AdPlan; +import com.njcn.gather.plan.pojo.po.PqDevCheckHistory; +import com.njcn.gather.plan.pojo.vo.PlanStatisticsItemVO; + +import java.util.List; + +public interface IPqDevCheckHistoryService extends IService { + + void saveOrUpdateDeviceHistory(AdPlan plan, PqDevVO device); + + void backfillPlanHistoryIfEmpty(AdPlan plan, List checkedDevices); + + List listItemDistributions(String planId, String manufacturer, String devType); +} diff --git a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java index 6f4a25d7..d1b36972 100644 --- a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java @@ -59,6 +59,7 @@ import com.njcn.gather.plan.service.IAdPlanService; import com.njcn.gather.plan.service.IAdPlanSourceService; import com.njcn.gather.plan.service.IAdPlanStandardDevService; import com.njcn.gather.plan.service.IAdPlanTestConfigService; +import com.njcn.gather.plan.service.IPqDevCheckHistoryService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.report.pojo.po.PqReport; import com.njcn.gather.report.service.IPqReportService; @@ -150,6 +151,7 @@ public class AdPlanServiceImpl extends ServiceImpl impleme private final IAdPariService adPairService; private final IAdPlanTestConfigService adPlanTestConfigService; private final ISysUserService sysUserService; + private final IPqDevCheckHistoryService pqDevCheckHistoryService; private final JdbcTemplate jdbcTemplate; @Value("${report.reportDir}") @@ -816,19 +818,21 @@ public class AdPlanServiceImpl extends ServiceImpl impleme } @Override - public PlanStatisticsVO statistics(String planId) { + public PlanStatisticsVO statistics(AdPlanParam.StatisticsParam param) { + String planId = param.getPlanId(); AdPlan plan = this.getById(planId); if (ObjectUtil.isNull(plan)) { throw new BusinessException(DetectionResponseEnum.PLAN_NOT_EXIST); } - if (!CheckStateEnum.CHECKED.getValue().equals(plan.getTestState())) { - throw new BusinessException(DetectionResponseEnum.NOT_CHECKED_PLAN_CANNOT_ANALYSE); - } - List checkedDevices = listCheckedDevices(plan.getId()); + List planDevices = listPlanDevices(plan.getId(), param.getManufacturer(), param.getDevType()); + List checkedDevices = planDevices.stream() + .filter(this::isCheckedDevice) + .collect(Collectors.toList()); PlanStatisticsVO statistics = new PlanStatisticsVO(); statistics.setPlanId(plan.getId()); statistics.setPlanName(plan.getName()); + statistics.setUncheckedDeviceCount(planDevices.size() - checkedDevices.size()); statistics.setCheckedDeviceCount(checkedDevices.size()); statistics.setTotalCheckCount(checkedDevices.stream().mapToInt(dev -> defaultZero(dev.getRecheckNum())).sum()); @@ -851,13 +855,15 @@ public class AdPlanServiceImpl extends ServiceImpl impleme statistics.setFirstQualifiedDeviceCount(firstQualifiedCount); statistics.setSecondQualifiedDeviceCount(secondQualifiedCount); statistics.setThirdOrMoreQualifiedDeviceCount(thirdOrMoreQualifiedCount); + statistics.setQualifiedDeviceCount(firstQualifiedCount + secondQualifiedCount + thirdOrMoreQualifiedCount); statistics.setUnqualifiedDeviceCount(unqualifiedDeviceCount); statistics.setFirstPassRate(rate(firstQualifiedCount, checkedDevices.size())); statistics.setSecondPassRate(rate(secondQualifiedCount, checkedDevices.size())); statistics.setThirdOrMorePassRate(rate(thirdOrMoreQualifiedCount, checkedDevices.size())); statistics.setUnqualifiedRate(rate(unqualifiedDeviceCount, checkedDevices.size())); - List itemDistributions = buildItemDistributions(plan, checkedDevices); + pqDevCheckHistoryService.backfillPlanHistoryIfEmpty(plan, checkedDevices); + List itemDistributions = pqDevCheckHistoryService.listItemDistributions(plan.getId(), param.getManufacturer(), param.getDevType()); statistics.setItemDistributions(itemDistributions); statistics.setUnqualifiedItemCount(itemDistributions.stream() .mapToInt(item -> defaultZero(item.getUnqualifiedCount())) @@ -865,7 +871,7 @@ public class AdPlanServiceImpl extends ServiceImpl impleme return statistics; } - private List listCheckedDevices(String planId) { + private List listPlanDevices(String planId, String manufacturer, String devType) { PqDevParam.QueryParam queryParam = new PqDevParam.QueryParam(); queryParam.setPlanIdList(Collections.singletonList(planId)); List pqDevVOList = pqDevMapper.selectByQueryParam(queryParam); @@ -873,10 +879,17 @@ public class AdPlanServiceImpl extends ServiceImpl impleme return Collections.emptyList(); } return pqDevVOList.stream() - .filter(dev -> !Objects.equals(dev.getCheckResult(), CheckResultEnum.UNCHECKED.getValue())) + .filter(dev -> StrUtil.isBlank(manufacturer) || Objects.equals(dev.getManufacturer(), manufacturer)) + .filter(dev -> StrUtil.isBlank(devType) || Objects.equals(dev.getDevType(), devType)) .collect(Collectors.toList()); } + private boolean isCheckedDevice(PqDevVO dev) { + return (Objects.equals(dev.getCheckState(), CheckStateEnum.CHECKED.getValue()) + || Objects.equals(dev.getCheckState(), CheckStateEnum.DOCUMENTED.getValue())) + && !Objects.equals(dev.getCheckResult(), CheckResultEnum.UNCHECKED.getValue()); + } + private List buildItemDistributions(AdPlan plan, List checkedDevices) { if (CollUtil.isEmpty(checkedDevices) || StrUtil.isBlank(plan.getScriptId()) || ObjectUtil.isNull(plan.getCode())) { return Collections.emptyList(); @@ -1017,10 +1030,7 @@ public class AdPlanServiceImpl extends ServiceImpl impleme PlanStatisticsItemVO itemVO = new PlanStatisticsItemVO(); itemVO.setItemId(itemId); itemVO.setItemName(itemName); - itemVO.setTotalCount(totalCount); - itemVO.setQualifiedCount(qualifiedCount); itemVO.setUnqualifiedCount(unqualifiedCount); - itemVO.setPassRate(rate(qualifiedCount, totalCount)); return itemVO; } } diff --git a/detection/src/main/java/com/njcn/gather/plan/service/impl/PqDevCheckHistoryServiceImpl.java b/detection/src/main/java/com/njcn/gather/plan/service/impl/PqDevCheckHistoryServiceImpl.java new file mode 100644 index 00000000..2cf58931 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/plan/service/impl/PqDevCheckHistoryServiceImpl.java @@ -0,0 +1,220 @@ +package com.njcn.gather.plan.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.gather.device.pojo.vo.PqDevVO; +import com.njcn.gather.plan.mapper.PqDevCheckHistoryMapper; +import com.njcn.gather.plan.pojo.po.AdPlan; +import com.njcn.gather.plan.pojo.po.PqDevCheckHistory; +import com.njcn.gather.plan.pojo.vo.PlanStatisticsItemVO; +import com.njcn.gather.plan.service.IPqDevCheckHistoryService; +import com.njcn.gather.script.mapper.PqScriptDtlsMapper; +import com.njcn.gather.script.pojo.po.PqScriptDtls; +import com.njcn.gather.storage.pojo.po.SimAndDigBaseResult; +import com.njcn.gather.storage.service.SimAndDigHarmonicService; +import com.njcn.gather.storage.service.SimAndDigNonHarmonicService; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; +import com.njcn.gather.system.dictionary.service.IDictTreeService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PqDevCheckHistoryServiceImpl extends ServiceImpl implements IPqDevCheckHistoryService { + + private final SimAndDigNonHarmonicService adNonHarmonicService; + private final SimAndDigHarmonicService adHarmonicService; + private final PqScriptDtlsMapper pqScriptDtlsMapper; + private final IDictTreeService dictTreeService; + + @Override + @Transactional + public void saveOrUpdateDeviceHistory(AdPlan plan, PqDevVO device) { + if (ObjectUtil.isNull(plan) + || ObjectUtil.isNull(device) + || StrUtil.isBlank(plan.getScriptId()) + || ObjectUtil.isNull(plan.getCode()) + || StrUtil.isBlank(device.getId()) + || ObjectUtil.isNull(device.getRecheckNum())) { + return; + } + + Map scriptItemInfoMap = getScriptItemInfoMap(plan.getScriptId()); + if (CollUtil.isEmpty(scriptItemInfoMap)) { + return; + } + + List resultList = new ArrayList<>(); + resultList.addAll(adNonHarmonicService.listSimAndDigBaseResult(plan.getScriptId(), plan.getCode() + "", device.getId())); + resultList.addAll(adHarmonicService.listAllResultData(plan.getScriptId(), plan.getCode() + "", device.getId())); + if (CollUtil.isEmpty(resultList)) { + return; + } + + Map> itemResultMap = resultList.stream() + .filter(result -> ObjectUtil.isNotNull(result.getSort())) + .map(result -> new AbstractMap.SimpleEntry<>(scriptItemInfoMap.get(result.getSort()), result)) + .filter(entry -> ObjectUtil.isNotNull(entry.getKey())) + .collect(Collectors.groupingBy( + entry -> entry.getKey().getItemId(), + TreeMap::new, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()) + )); + + LocalDateTime now = LocalDateTime.now(); + itemResultMap.forEach((itemId, results) -> { + boolean hasUnqualified = results.stream().anyMatch(result -> isUnqualifiedResultFlag(result.getResultFlag())); + boolean hasQualified = results.stream().anyMatch(result -> Objects.equals(result.getResultFlag(), 1)); + if (!hasUnqualified && !hasQualified) { + return; + } + + ScriptItemInfo itemInfo = scriptItemInfoMap.get(results.get(0).getSort()); + PqDevCheckHistory history = this.getOne(new LambdaQueryWrapper() + .eq(PqDevCheckHistory::getPlanId, plan.getId()) + .eq(PqDevCheckHistory::getDevId, device.getId()) + .eq(PqDevCheckHistory::getRecheckNum, device.getRecheckNum()) + .eq(PqDevCheckHistory::getItemId, itemId) + .last("LIMIT 1")); + + if (ObjectUtil.isNull(history)) { + history = new PqDevCheckHistory(); + history.setId(UUID.randomUUID().toString().replaceAll("-", "")); + history.setPlanId(plan.getId()); + history.setDevId(device.getId()); + history.setRecheckNum(device.getRecheckNum()); + history.setItemId(itemId); + history.setCreateBy(device.getCheckBy()); + history.setCreateTime(now); + } + + history.setDevType(device.getDevType()); + history.setManufacturer(device.getManufacturer()); + history.setItemName(itemInfo.getItemName()); + history.setResult(hasUnqualified ? 0 : 1); + history.setCheckTime(ObjectUtil.isNotNull(device.getCheckTime()) ? device.getCheckTime() : now); + history.setState(DataStateEnum.ENABLE.getCode()); + history.setUpdateBy(device.getCheckBy()); + history.setUpdateTime(now); + this.saveOrUpdate(history); + }); + } + + @Override + public void backfillPlanHistoryIfEmpty(AdPlan plan, List checkedDevices) { + if (ObjectUtil.isNull(plan) || CollUtil.isEmpty(checkedDevices)) { + return; + } + long historyCount = this.count(new LambdaQueryWrapper() + .eq(PqDevCheckHistory::getPlanId, plan.getId()) + .eq(PqDevCheckHistory::getState, DataStateEnum.ENABLE.getCode())); + if (historyCount > 0) { + return; + } + checkedDevices.forEach(device -> saveOrUpdateDeviceHistory(plan, device)); + } + + @Override + public List listItemDistributions(String planId, String manufacturer, String devType) { + List itemDistributions = this.baseMapper.listItemDistributions(planId, manufacturer, devType); + if (CollUtil.isEmpty(itemDistributions)) { + return Collections.emptyList(); + } + itemDistributions.forEach(item -> item.setUnqualifiedCount(defaultZero(item.getUnqualifiedCount()))); + return itemDistributions; + } + + private Map getScriptItemInfoMap(String scriptId) { + List scriptDtlsList = pqScriptDtlsMapper.selectList(new LambdaQueryWrapper() + .eq(PqScriptDtls::getScriptId, scriptId) + .ne(PqScriptDtls::getScriptIndex, -1) + .eq(PqScriptDtls::getEnable, DataStateEnum.ENABLE.getCode()) + ); + if (CollUtil.isEmpty(scriptDtlsList)) { + return Collections.emptyMap(); + } + + List scriptTypeIds = scriptDtlsList.stream() + .map(PqScriptDtls::getScriptType) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + Map dictTreeMap = Collections.emptyMap(); + if (CollUtil.isNotEmpty(scriptTypeIds)) { + dictTreeMap = dictTreeService.getDictTreeById(scriptTypeIds).stream() + .collect(Collectors.toMap(DictTree::getId, dictTree -> dictTree, (left, right) -> left)); + } + + Map finalDictTreeMap = dictTreeMap; + return scriptDtlsList.stream() + .collect(Collectors.groupingBy(PqScriptDtls::getScriptIndex, TreeMap::new, Collectors.toList())) + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> resolveScriptItemInfo(entry.getKey(), entry.getValue(), finalDictTreeMap), + (left, right) -> left, + TreeMap::new + )); + } + + private ScriptItemInfo resolveScriptItemInfo(Integer sort, List dtlsList, Map dictTreeMap) { + if (CollUtil.isEmpty(dtlsList)) { + return new ScriptItemInfo(String.valueOf(sort), "检测项" + sort); + } + PqScriptDtls first = dtlsList.get(0); + DictTree dictTree = dictTreeMap.get(first.getScriptType()); + if (ObjectUtil.isNotNull(dictTree) && StrUtil.isNotBlank(dictTree.getName())) { + return new ScriptItemInfo(dictTree.getId(), dictTree.getName()); + } + if (StrUtil.isNotBlank(first.getScriptType())) { + return new ScriptItemInfo(first.getScriptType(), "检测项" + sort); + } + return new ScriptItemInfo(String.valueOf(sort), "检测项" + sort); + } + + private boolean isUnqualifiedResultFlag(Integer resultFlag) { + return ObjectUtil.isNotNull(resultFlag) + && !Objects.equals(resultFlag, 1) + && !Objects.equals(resultFlag, 4) + && !Objects.equals(resultFlag, 5); + } + + private Integer defaultZero(Integer value) { + return ObjectUtil.isNull(value) ? 0 : value; + } + + private static class ScriptItemInfo { + private final String itemId; + private final String itemName; + + private ScriptItemInfo(String itemId, String itemName) { + this.itemId = itemId; + this.itemName = itemName; + } + + private String getItemId() { + return itemId; + } + + private String getItemName() { + return itemName; + } + } +} diff --git a/detection/src/test/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVOTest.java b/detection/src/test/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVOTest.java new file mode 100644 index 00000000..d2f04e60 --- /dev/null +++ b/detection/src/test/java/com/njcn/gather/plan/pojo/vo/PlanStatisticsVOTest.java @@ -0,0 +1,16 @@ +package com.njcn.gather.plan.pojo.vo; + +import org.junit.Assert; +import org.junit.Test; + +public class PlanStatisticsVOTest { + + @Test + public void shouldExposeQualifiedDeviceCount() { + PlanStatisticsVO statistics = new PlanStatisticsVO(); + + statistics.setQualifiedDeviceCount(6); + + Assert.assertEquals(Integer.valueOf(6), statistics.getQualifiedDeviceCount()); + } +} diff --git a/entrance/src/main/resources/application.yml b/entrance/src/main/resources/application.yml index 013c1438..aac8735c 100644 --- a/entrance/src/main/resources/application.yml +++ b/entrance/src/main/resources/application.yml @@ -1,15 +1,15 @@ server: - port: 18092 + port: 18093 spring: application: name: entrance datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver -# url: jdbc:mysql://192.168.1.24:13306/pqs91002?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true + url: jdbc:mysql://192.168.1.24:13306/pqs9100?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # username: root # password: njcnpqs - url: jdbc:mysql://127.0.0.1:3306/pqs9100?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true +# url: jdbc:mysql://127.0.0.1:3306/pqs9100?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: njcnpqs #初始化建立物理连接的个数、最小、最大连接数 @@ -60,7 +60,7 @@ socket: # port: 61000 webSocket: - port: 7777 + port: 7778 #源参数下发,暂态数据默认值 Dip: @@ -88,6 +88,8 @@ report: dateFormat: yyyy年MM月dd日 data: homeDir: D:\data +resource: + videoDir: ${data.homeDir}\resources\videos qr: cloud: http://pqmcc.com:18082/api/file dev: diff --git a/system/src/main/java/com/njcn/gather/system/resource/controller/ResourceManageController.java b/system/src/main/java/com/njcn/gather/system/resource/controller/ResourceManageController.java new file mode 100644 index 00000000..8c32b758 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/controller/ResourceManageController.java @@ -0,0 +1,86 @@ +package com.njcn.gather.system.resource.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +import com.njcn.common.pojo.constant.OperateType; +import com.njcn.common.pojo.enums.common.LogEnum; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.resource.pojo.param.ResourceManageParam; +import com.njcn.gather.system.resource.pojo.vo.PlayVO; +import com.njcn.gather.system.resource.pojo.vo.ResourceManageVO; +import com.njcn.gather.system.resource.service.IResourceManageService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; + +/** + * 资源管理 + */ +@Slf4j +@Api(tags = "资源管理") +@RestController +@RequestMapping("/resourceManage") +@RequiredArgsConstructor +public class ResourceManageController extends BaseController { + + private final IResourceManageService resourceManageService; + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @PostMapping("/list") + @ApiOperation("分页查询资源列表") + @ApiImplicitParam(name = "queryParam", value = "查询参数", required = true) + public HttpResult> list(@RequestBody @Validated ResourceManageParam.QueryParam queryParam) { + String methodDescribe = getMethodDescribe("list"); + LogUtil.njcnDebug(log, "{},查询参数为:{}", methodDescribe, queryParam); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, resourceManageService.list(queryParam), methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) + @PostMapping("/add") + @ApiOperation("新增视频资源") + @ApiImplicitParam(name = "resourceManageParam", value = "资源参数", required = true) + public HttpResult add(ResourceManageParam resourceManageParam) { + String methodDescribe = getMethodDescribe("add"); + LogUtil.njcnDebug(log, "{},新增资源参数为:{}", methodDescribe, resourceManageParam); + boolean result = resourceManageService.add(resourceManageParam); + if (result) { + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); + } + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.FAIL, false, methodDescribe); + } + + @OperateInfo(info = LogEnum.SYSTEM_COMMON) + @GetMapping("/play") + @ApiOperation("获取视频播放地址") + @ApiImplicitParam(name = "id", value = "资源id", required = true) + public HttpResult play(@RequestParam("id") String id) { + String methodDescribe = getMethodDescribe("play"); + LogUtil.njcnDebug(log, "{},资源id为:{}", methodDescribe, id); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, resourceManageService.play(id), methodDescribe); + } + + @GetMapping("/stream") + @ApiOperation("播放视频流") + public void stream(@RequestParam("id") String id, + @RequestParam("token") String token, + @RequestHeader(value = "Range", required = false) String rangeHeader, + HttpServletResponse response) { + resourceManageService.stream(id, token, rangeHeader, response); + } +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/mapper/ResourceManageMapper.java b/system/src/main/java/com/njcn/gather/system/resource/mapper/ResourceManageMapper.java new file mode 100644 index 00000000..3f7885d6 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/mapper/ResourceManageMapper.java @@ -0,0 +1,10 @@ +package com.njcn.gather.system.resource.mapper; + +import com.github.yulichang.base.MPJBaseMapper; +import com.njcn.gather.system.resource.pojo.po.ResourceManage; + +/** + * 资源管理 mapper + */ +public interface ResourceManageMapper extends MPJBaseMapper { +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/mapper/mapping/ResourceManageMapper.xml b/system/src/main/java/com/njcn/gather/system/resource/mapper/mapping/ResourceManageMapper.xml new file mode 100644 index 00000000..bfbe2a23 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/mapper/mapping/ResourceManageMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/system/src/main/java/com/njcn/gather/system/resource/pojo/enums/ResourceManageResponseEnum.java b/system/src/main/java/com/njcn/gather/system/resource/pojo/enums/ResourceManageResponseEnum.java new file mode 100644 index 00000000..760199bb --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/pojo/enums/ResourceManageResponseEnum.java @@ -0,0 +1,31 @@ +package com.njcn.gather.system.resource.pojo.enums; + +import lombok.Getter; + +/** + * 资源管理业务响应 + */ +@Getter +public enum ResourceManageResponseEnum { + + NAME_NOT_BLANK("A013001", "资源名称不能为空"), + REMARK_NOT_BLANK("A013002", "备注不能为空"), + FILE_NOT_NULL("A013003", "上传的视频文件不能为空"), + FILE_SUFFIX_ERROR("A013004", "仅支持上传 MP4 文件"), + FILE_SIZE_ERROR("A013005", "文件大小不能超过 250MB"), + FILE_UPLOAD_FAILED("A013006", "文件上传失败"), + RESOURCE_NOT_EXIST("A013007", "资源不存在"), + RESOURCE_FILE_NOT_EXIST("A013008", "资源文件不存在"), + PLAY_TOKEN_INVALID("A013009", "播放授权无效"), + PLAY_TOKEN_EXPIRED("A013010", "播放授权已过期"), + RANGE_INVALID("A013011", "视频请求范围非法"), + DISK_SPACE_NOT_ENOUGH("A013012", "磁盘空间不足"); + + private final String code; + private final String message; + + ResourceManageResponseEnum(String code, String message) { + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/system/src/main/java/com/njcn/gather/system/resource/pojo/param/ResourceManageParam.java b/system/src/main/java/com/njcn/gather/system/resource/pojo/param/ResourceManageParam.java new file mode 100644 index 00000000..0c631b3f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/pojo/param/ResourceManageParam.java @@ -0,0 +1,33 @@ +package com.njcn.gather.system.resource.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.web.multipart.MultipartFile; + +/** + * 资源管理参数 + */ +@Data +public class ResourceManageParam { + + @ApiModelProperty(value = "资源名称", required = true) + private String name; + + @ApiModelProperty(value = "备注", required = true) + private String remark; + + @ApiModelProperty(value = "视频文件", required = true) + private MultipartFile file; + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("资源名称") + private String name; + + @ApiModelProperty("原始文件名") + private String fileName; + } +} \ No newline at end of file diff --git a/system/src/main/java/com/njcn/gather/system/resource/pojo/po/ResourceManage.java b/system/src/main/java/com/njcn/gather/system/resource/pojo/po/ResourceManage.java new file mode 100644 index 00000000..e68edb55 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/pojo/po/ResourceManage.java @@ -0,0 +1,32 @@ +package com.njcn.gather.system.resource.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.njcn.db.mybatisplus.bo.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; + +/** + * 资源管理 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_resource_manage") +public class ResourceManage extends BaseEntity implements Serializable { + private static final long serialVersionUID = 684206384930125506L; + + private String id; + + private String name; + + private String fileName; + + private Long fileSize; + + private String relativePath; + + private String remark; + + private Integer state; +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/PlayVO.java b/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/PlayVO.java new file mode 100644 index 00000000..4d595178 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/PlayVO.java @@ -0,0 +1,15 @@ +package com.njcn.gather.system.resource.pojo.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 播放授权信息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PlayVO { + private String url; +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/ResourceManageVO.java b/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/ResourceManageVO.java new file mode 100644 index 00000000..15e5df3b --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/pojo/vo/ResourceManageVO.java @@ -0,0 +1,33 @@ +package com.njcn.gather.system.resource.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 资源管理列表数据 + */ +@Data +public class ResourceManageVO { + private String id; + + private String name; + + private String fileName; + + private Long fileSize; + + private String relativePath; + + private String remark; + + private Integer state; + + private String createBy; + + private LocalDateTime createTime; + + private String updateBy; + + private LocalDateTime updateTime; +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/service/IResourceManageService.java b/system/src/main/java/com/njcn/gather/system/resource/service/IResourceManageService.java new file mode 100644 index 00000000..7440d382 --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/service/IResourceManageService.java @@ -0,0 +1,24 @@ +package com.njcn.gather.system.resource.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.system.resource.pojo.param.ResourceManageParam; +import com.njcn.gather.system.resource.pojo.po.ResourceManage; +import com.njcn.gather.system.resource.pojo.vo.PlayVO; +import com.njcn.gather.system.resource.pojo.vo.ResourceManageVO; + +import javax.servlet.http.HttpServletResponse; + +/** + * 资源管理服务 + */ +public interface IResourceManageService extends IService { + + Page list(ResourceManageParam.QueryParam queryParam); + + boolean add(ResourceManageParam resourceManageParam); + + PlayVO play(String id); + + void stream(String id, String token, String rangeHeader, HttpServletResponse response); +} diff --git a/system/src/main/java/com/njcn/gather/system/resource/service/impl/ResourceManageServiceImpl.java b/system/src/main/java/com/njcn/gather/system/resource/service/impl/ResourceManageServiceImpl.java new file mode 100644 index 00000000..92d96f0f --- /dev/null +++ b/system/src/main/java/com/njcn/gather/system/resource/service/impl/ResourceManageServiceImpl.java @@ -0,0 +1,359 @@ +package com.njcn.gather.system.resource.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.common.DataStateEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.system.resource.mapper.ResourceManageMapper; +import com.njcn.gather.system.resource.pojo.enums.ResourceManageResponseEnum; +import com.njcn.gather.system.resource.pojo.param.ResourceManageParam; +import com.njcn.gather.system.resource.pojo.po.ResourceManage; +import com.njcn.gather.system.resource.pojo.vo.PlayVO; +import com.njcn.gather.system.resource.pojo.vo.ResourceManageVO; +import com.njcn.gather.system.resource.service.IResourceManageService; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.RequestUtil; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class ResourceManageServiceImpl extends ServiceImpl implements IResourceManageService { + + private static final long MAX_FILE_SIZE = 250L * 1024L * 1024L; + private static final long PLAY_TOKEN_TTL_MILLIS = 30L * 60L * 1000L; + private static final String MP4_SUFFIX = ".mp4"; + private static final String RELATIVE_VIDEO_ROOT = "resources/videos"; + private static final int BUFFER_SIZE = 8192; + + private static final ConcurrentHashMap PLAY_TOKEN_CACHE = new ConcurrentHashMap<>(); + + @Value("${resource.videoDir:D:/data/resources/videos}") + private String videoDir; + + @Override + public Page list(ResourceManageParam.QueryParam queryParam) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.like(StrUtil.isNotBlank(queryParam.getName()), "Name", queryParam.getName()) + .like(StrUtil.isNotBlank(queryParam.getFileName()), "File_Name", queryParam.getFileName()) + .eq("State", DataStateEnum.ENABLE.getCode()) + .orderByDesc("Create_Time"); + + Page page = this.page( + new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)), + wrapper + ); + + List records = page.getRecords().stream().map(resource -> { + ResourceManageVO vo = new ResourceManageVO(); + BeanUtil.copyProperties(resource, vo); + return vo; + }).collect(Collectors.toList()); + + Page result = new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)); + result.setTotal(page.getTotal()); + result.setPages(page.getPages()); + result.setCurrent(page.getCurrent()); + result.setSize(page.getSize()); + result.setRecords(records); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean add(ResourceManageParam resourceManageParam) { + validateAddParam(resourceManageParam); + + MultipartFile file = resourceManageParam.getFile(); + String originalFilename = file.getOriginalFilename(); + String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String savedFileName = UUID.randomUUID().toString().replace("-", "") + MP4_SUFFIX; + Path rootPath = Paths.get(videoDir).toAbsolutePath().normalize(); + Path targetDir = rootPath.resolve(dateDir).normalize(); + Path targetFile = targetDir.resolve(savedFileName).normalize(); + ensurePathInRoot(rootPath, targetFile); + + String relativePath = RELATIVE_VIDEO_ROOT + "/" + dateDir + "/" + savedFileName; + + try { + Files.createDirectories(targetDir); + long usableSpace = targetDir.toFile().getUsableSpace(); + if (usableSpace > 0 && usableSpace < file.getSize()) { + throw new BusinessException(ResourceManageResponseEnum.DISK_SPACE_NOT_ENOUGH); + } + file.transferTo(targetFile.toFile()); + } catch (BusinessException e) { + throw e; + } catch (IOException e) { + throw new BusinessException(ResourceManageResponseEnum.FILE_UPLOAD_FAILED); + } + + ResourceManage resourceManage = new ResourceManage(); + resourceManage.setId(UUID.randomUUID().toString().replace("-", "")); + resourceManage.setName(resourceManageParam.getName().trim()); + resourceManage.setRemark(resourceManageParam.getRemark().trim()); + resourceManage.setFileName(originalFilename); + resourceManage.setFileSize(file.getSize()); + resourceManage.setRelativePath(relativePath); + resourceManage.setState(DataStateEnum.ENABLE.getCode()); + + try { + boolean saved = this.save(resourceManage); + if (!saved) { + deleteQuietly(targetFile); + } + return saved; + } catch (RuntimeException e) { + deleteQuietly(targetFile); + throw e; + } + } + + @Override + public PlayVO play(String id) { + ResourceManage resourceManage = getEnabledResource(id); + Path filePath = resolveResourcePath(resourceManage); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST); + } + + String token = UUID.randomUUID().toString().replace("-", ""); + PlayToken playToken = new PlayToken(); + playToken.setResourceId(id); + playToken.setUserId(RequestUtil.getUserId()); + playToken.setExpireTime(System.currentTimeMillis() + PLAY_TOKEN_TTL_MILLIS); + PLAY_TOKEN_CACHE.put(token, playToken); + clearExpiredTokens(); + + return new PlayVO("/resourceManage/stream?id=" + id + "&token=" + token); + } + + @Override + public void stream(String id, String token, String rangeHeader, HttpServletResponse response) { + if (!validatePlayToken(id, token)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + + ResourceManage resourceManage = getEnabledResource(id); + Path filePath = resolveResourcePath(resourceManage); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + + try { + long fileLength = Files.size(filePath); + Range range = parseRange(rangeHeader, fileLength); + String encodedFileName = URLEncoder.encode(resourceManage.getFileName(), "UTF-8") + .replaceAll("\\+", "%20"); + + response.setContentType("video/mp4"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Disposition", "inline; filename*=UTF-8''" + encodedFileName); + response.setHeader("Content-Length", String.valueOf(range.getLength())); + if (range.isPartial()) { + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + response.setHeader("Content-Range", "bytes " + range.getStart() + "-" + range.getEnd() + "/" + fileLength); + } else { + response.setStatus(HttpServletResponse.SC_OK); + } + + writeRange(filePath, range, response); + } catch (IllegalArgumentException e) { + response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + } catch (IOException e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + private void validateAddParam(ResourceManageParam param) { + if (param == null || StrUtil.isBlank(param.getName())) { + throw new BusinessException(ResourceManageResponseEnum.NAME_NOT_BLANK); + } + if (StrUtil.isBlank(param.getRemark())) { + throw new BusinessException(ResourceManageResponseEnum.REMARK_NOT_BLANK); + } + MultipartFile file = param.getFile(); + if (file == null || file.isEmpty()) { + throw new BusinessException(ResourceManageResponseEnum.FILE_NOT_NULL); + } + String originalFilename = file.getOriginalFilename(); + if (StrUtil.isBlank(originalFilename) || !originalFilename.toLowerCase().endsWith(MP4_SUFFIX)) { + throw new BusinessException(ResourceManageResponseEnum.FILE_SUFFIX_ERROR); + } + String contentType = file.getContentType(); + if (StrUtil.isNotBlank(contentType) && !"video/mp4".equalsIgnoreCase(contentType) + && !"application/octet-stream".equalsIgnoreCase(contentType)) { + throw new BusinessException(ResourceManageResponseEnum.FILE_SUFFIX_ERROR); + } + if (file.getSize() > MAX_FILE_SIZE) { + throw new BusinessException(ResourceManageResponseEnum.FILE_SIZE_ERROR); + } + } + + private ResourceManage getEnabledResource(String id) { + ResourceManage resourceManage = this.lambdaQuery() + .eq(ResourceManage::getId, id) + .eq(ResourceManage::getState, DataStateEnum.ENABLE.getCode()) + .one(); + if (resourceManage == null) { + throw new BusinessException(ResourceManageResponseEnum.RESOURCE_NOT_EXIST); + } + return resourceManage; + } + + private Path resolveResourcePath(ResourceManage resourceManage) { + Path rootPath = Paths.get(videoDir).toAbsolutePath().normalize(); + String relativePath = resourceManage.getRelativePath().replace("\\", "/"); + String prefix = RELATIVE_VIDEO_ROOT + "/"; + if (!relativePath.startsWith(prefix)) { + throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST); + } + String relativeToVideoRoot = relativePath.substring(prefix.length()); + Path filePath = rootPath.resolve(relativeToVideoRoot).normalize(); + ensurePathInRoot(rootPath, filePath); + return filePath; + } + + private void ensurePathInRoot(Path rootPath, Path targetPath) { + if (!targetPath.startsWith(rootPath)) { + throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST); + } + } + + private boolean validatePlayToken(String id, String token) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(token)) { + return false; + } + PlayToken playToken = PLAY_TOKEN_CACHE.get(token); + if (playToken == null || !id.equals(playToken.getResourceId())) { + return false; + } + if (playToken.getExpireTime() < System.currentTimeMillis()) { + PLAY_TOKEN_CACHE.remove(token); + return false; + } + return true; + } + + private void clearExpiredTokens() { + long now = System.currentTimeMillis(); + PLAY_TOKEN_CACHE.entrySet().removeIf(entry -> entry.getValue().getExpireTime() < now); + } + + private Range parseRange(String rangeHeader, long fileLength) { + if (StrUtil.isBlank(rangeHeader)) { + return new Range(0, fileLength - 1, false); + } + if (!rangeHeader.startsWith("bytes=")) { + throw new IllegalArgumentException("Invalid range"); + } + String rangeValue = rangeHeader.substring("bytes=".length()); + String[] parts = rangeValue.split("-", -1); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid range"); + } + + long start; + long end; + if (StrUtil.isBlank(parts[0])) { + long suffixLength = Long.parseLong(parts[1]); + if (suffixLength <= 0) { + throw new IllegalArgumentException("Invalid range"); + } + start = Math.max(fileLength - suffixLength, 0); + end = fileLength - 1; + } else { + start = Long.parseLong(parts[0]); + end = StrUtil.isBlank(parts[1]) ? fileLength - 1 : Long.parseLong(parts[1]); + } + + if (start < 0 || end < start || start >= fileLength) { + throw new IllegalArgumentException("Invalid range"); + } + end = Math.min(end, fileLength - 1); + return new Range(start, end, true); + } + + private void writeRange(Path filePath, Range range, HttpServletResponse response) throws IOException { + try (InputStream inputStream = Files.newInputStream(filePath); + ServletOutputStream outputStream = response.getOutputStream()) { + long skipped = inputStream.skip(range.getStart()); + while (skipped < range.getStart()) { + long current = inputStream.skip(range.getStart() - skipped); + if (current <= 0) { + break; + } + skipped += current; + } + + byte[] buffer = new byte[BUFFER_SIZE]; + long bytesRemaining = range.getLength(); + while (bytesRemaining > 0) { + int readLength = (int) Math.min(buffer.length, bytesRemaining); + int read = inputStream.read(buffer, 0, readLength); + if (read == -1) { + break; + } + outputStream.write(buffer, 0, read); + bytesRemaining -= read; + } + outputStream.flush(); + } + } + + private void deleteQuietly(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + log.warn("删除资源文件失败: {}", path, e); + } + } + + @Data + private static class PlayToken { + private String resourceId; + private String userId; + private long expireTime; + } + + @Data + private static class Range { + private final long start; + private final long end; + private final boolean partial; + + private long getLength() { + return end - start + 1; + } + } +} + + + diff --git a/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java index 591803ad..839594f2 100644 --- a/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java +++ b/user/src/main/java/com/njcn/gather/user/user/filter/AuthGlobalFilter.java @@ -25,7 +25,7 @@ import java.util.List; @Slf4j @Component public class AuthGlobalFilter implements Filter, Ordered { - private final static List IGNORE_URI = Arrays.asList("/doc.html","/v3/api-docs","/admin/login","/admin/getPublicKey", "/report/generateReport"); + private final static List IGNORE_URI = Arrays.asList("/doc.html","/v3/api-docs","/admin/login","/admin/getPublicKey", "/report/generateReport", "/resourceManage/stream"); @Override public int getOrder() {