diff --git a/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java b/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java new file mode 100644 index 0000000..451d8ee --- /dev/null +++ b/src/main/java/com/ycwl/basic/biz/TaskStatusBiz.java @@ -0,0 +1,192 @@ +package com.ycwl.basic.biz; + +import com.ycwl.basic.mapper.FaceMapper; +import com.ycwl.basic.mapper.TaskMapper; +import com.ycwl.basic.mapper.VideoMapper; +import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO; +import com.ycwl.basic.model.pc.face.entity.FaceEntity; +import com.ycwl.basic.model.pc.face.resp.FaceRespVO; +import com.ycwl.basic.model.pc.task.req.TaskReqQuery; +import com.ycwl.basic.model.pc.task.resp.TaskRespVO; +import com.ycwl.basic.model.pc.template.resp.TemplateRespVO; +import com.ycwl.basic.model.pc.video.entity.VideoEntity; +import com.ycwl.basic.repository.FaceRepository; +import com.ycwl.basic.repository.TemplateRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +@Component +public class TaskStatusBiz { + public static final String TASK_STATUS_USER_CACHE_KEY = "task:status:user:%s:face:%s"; + public static final String TASK_STATUS_FACE_CACHE_KEY = "task:status:face:%s"; + public static final String TASK_STATUS_FACE_CACHE_KEY_CUT = "task:status:face:%s:cut"; + public static final String TASK_STATUS_FACE_CACHE_KEY_TEMPLATE = "task:status:face:%s:tpl:%s"; + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private FaceRepository faceRepository; + @Autowired + private TemplateRepository templateRepository; + @Autowired + private FaceMapper faceMapper; + @Autowired + private TaskMapper taskMapper; + @Autowired + private VideoMapper videoMapper; + @Autowired + private TemplateBiz templateBiz; + + public boolean getUserHaveFace(Long userId, Long faceId) { + if (userId == null || faceId == null) { + return false; + } + if (redisTemplate.hasKey(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId))) { + return true; + } + FaceEntity face = faceRepository.getFace(faceId); + if (face == null) { + return false; + } + if (face.getMemberId().equals(userId)) { + redisTemplate.opsForValue().set(String.format(TASK_STATUS_USER_CACHE_KEY, userId, faceId), "1", 3600, TimeUnit.SECONDS); + return true; + } else { + return false; + } + } + + public void setFaceCutStatus(Long faceId, int status) { + redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId), String.valueOf(status), 3600, TimeUnit.SECONDS); + } + + public void setFaceTemplateStatus(Long faceId, Long templateId, Long videoId) { + redisTemplate.opsForValue().set(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId), String.valueOf(videoId), 3600, TimeUnit.SECONDS); + } + + public VideoTaskStatusVO getScenicUserStatus(Long scenicId, Long userId) { + FaceRespVO lastFace = faceMapper.findLastFaceByScenicAndUserId(scenicId, userId); + VideoTaskStatusVO response = new VideoTaskStatusVO(); + if (lastFace == null) { + response.setStatus(-1); + return response; + } + return getFaceStatus(lastFace.getId()); + } + + public VideoTaskStatusVO getFaceStatus(Long faceId) { + FaceEntity face = faceRepository.getFace(faceId); + VideoTaskStatusVO response = new VideoTaskStatusVO(); + if (face == null) { + response.setStatus(-1); + return response; + } + response.setScenicId(face.getScenicId()); + response.setFaceId(faceId); + List templateList = templateRepository.getTemplateListByScenicId(face.getScenicId()); + response.setMaxCount(templateList.size()); + int alreadyFinished = 0; + for (TemplateRespVO template : templateList) { + response.setTemplateId(template.getId()); + long videoId = getFaceTemplateVideoId(faceId, template.getId()); + if (videoId <= 0) { + response.setStatus(2); + } else { + response.setVideoId(videoId); + alreadyFinished++; + } + } + response.setCount(alreadyFinished); + if (alreadyFinished == 0) { + response.setStatus(0); + } else { + response.setStatus(1); + } + if (alreadyFinished == 0) { + int faceCutStatus = getFaceCutStatus(faceId); + if (faceCutStatus != 1) { + // 正在切片 + if (templateBiz.determineTemplateCanGenerate(templateList.get(0).getId(), faceId, false)) { + response.setStatus(2); + } else { + response.setStatus(0); + } + } + } + return response; + } + + public VideoTaskStatusVO getFaceTemplateStatus(Long faceId, Long templateId) { + FaceEntity face = faceRepository.getFace(faceId); + VideoTaskStatusVO response = new VideoTaskStatusVO(); + if (face == null) { + response.setStatus(-1); + return response; + } + response.setScenicId(face.getScenicId()); + response.setFaceId(faceId); + response.setTemplateId(templateId); + long videoId = getFaceTemplateVideoId(faceId, templateId); + if (videoId < 0) { + int faceCutStatus = getFaceCutStatus(faceId); + if (faceCutStatus != 1) { + // 正在切片 + response.setStatus(2); + return response; + } + } else if (videoId == 0) { + response.setStatus(2); + } else { + response.setVideoId(videoId); + response.setStatus(1); + } + return response; + } + + public int getFaceCutStatus(Long faceId) { + if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId))) { + String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_CUT, faceId)); + if (status != null) { + return Integer.parseInt(status); + } + } + return 1; + } + + public long getFaceTemplateVideoId(Long faceId, Long templateId) { + if (redisTemplate.hasKey(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId))) { + String status = redisTemplate.opsForValue().get(String.format(TASK_STATUS_FACE_CACHE_KEY_TEMPLATE, faceId, templateId)); + if (status != null) { + return Long.parseLong(status); + } + } + TaskReqQuery taskReqQuery = new TaskReqQuery(); + taskReqQuery.setFaceId(faceId); + taskReqQuery.setTemplateId(templateId); + List list = taskMapper.list(taskReqQuery); + Optional min = list.stream().min(Comparator.comparing(TaskRespVO::getCreateTime)); + if (min.isPresent()) { + TaskRespVO task = min.get(); + long taskStatus = 0; + if (task.getStatus() == 1) { + // 已完成 + VideoEntity video = videoMapper.findByTaskId(task.getId()); + if (video != null) { + taskStatus = video.getId(); + } + } + setFaceTemplateStatus(faceId, templateId, taskStatus); + } else { + // 从来没生成过 + setFaceTemplateStatus(faceId, templateId, -1L); + return -1; + } + return 0; + } +} diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java index a1caf6a..160c07d 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppGoodsController.java @@ -1,6 +1,7 @@ package com.ycwl.basic.controller.mobile; import com.ycwl.basic.annotation.IgnoreToken; +import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.constant.BaseContextHandler; import com.ycwl.basic.exception.CheckTokenException; import com.ycwl.basic.model.jwt.JwtInfo; @@ -31,6 +32,8 @@ public class AppGoodsController { private GoodsService goodsService; @Autowired private TaskService taskService; + @Autowired + private TaskStatusBiz taskStatusBiz; @ApiOperation("商品列表") @PostMapping("/goodsList") diff --git a/src/main/java/com/ycwl/basic/controller/mobile/AppTaskController.java b/src/main/java/com/ycwl/basic/controller/mobile/AppTaskController.java index 0e63d13..90be752 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/AppTaskController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/AppTaskController.java @@ -1,6 +1,7 @@ package com.ycwl.basic.controller.mobile; import com.ycwl.basic.annotation.IgnoreLogReq; +import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.model.jwt.JwtInfo; import com.ycwl.basic.model.mobile.goods.VideoTaskReq; import com.ycwl.basic.model.mobile.goods.VideoTaskStatusVO; @@ -24,6 +25,8 @@ public class AppTaskController { private GoodsService goodsService; @Autowired private TaskService taskService; + @Autowired + private TaskStatusBiz taskStatusBiz; @GetMapping("/face/{faceId}") @IgnoreLogReq diff --git a/src/main/java/com/ycwl/basic/controller/viid/ViidController.java b/src/main/java/com/ycwl/basic/controller/viid/ViidController.java index 06424d8..74c65dc 100644 --- a/src/main/java/com/ycwl/basic/controller/viid/ViidController.java +++ b/src/main/java/com/ycwl/basic/controller/viid/ViidController.java @@ -273,7 +273,9 @@ public class ViidController { faceSampleMapper.add(faceSample); new Thread(() -> { taskFaceService.addFaceSample(faceSample.getId()); - DynamicTaskGenerator.addTask(faceSample.getId()); + if (deviceConfig != null && Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) { + DynamicTaskGenerator.addTask(faceSample.getId()); + } }).start(); for (SubImageInfoObject _subImage : type14ImageList) { facePosition.setImgHeight(_subImage.getHeight()); @@ -318,7 +320,12 @@ public class ViidController { faceSample.setFaceUrl(url); faceSampleMapper.add(faceSample); DynamicTaskGenerator.addTask(faceSample.getId()); - taskFaceService.addFaceSample(faceSample.getId()); + new Thread(() -> { + taskFaceService.addFaceSample(faceSample.getId()); + if (deviceConfig != null && Integer.valueOf(1).equals(deviceConfig.getEnablePreBook())) { + DynamicTaskGenerator.addTask(faceSample.getId()); + } + }).start(); log.info("模式1人脸信息入库成功!设备ID:{}", deviceID); } } diff --git a/src/main/java/com/ycwl/basic/enums/StatisticEnum.java b/src/main/java/com/ycwl/basic/enums/StatisticEnum.java index a41b744..49ce5b5 100644 --- a/src/main/java/com/ycwl/basic/enums/StatisticEnum.java +++ b/src/main/java/com/ycwl/basic/enums/StatisticEnum.java @@ -12,8 +12,9 @@ public enum StatisticEnum { REFUND(5,"退款"), MESSAGE_PUSH(6,"消息推送"), DOWNLOAD(8,"下载"), - CLICK_ON_PAYMENT(9,"点击支付、购买"), + CLICK_PAY(9,"点击支付"), OTHER_ENTER(10,"其他渠道进入"), + SUBMIT_PAYMENT(11,"点击支付"), SCAN_MARKED_CODE(20,"扫描特殊标记码"), ; diff --git a/src/main/java/com/ycwl/basic/interceptor/AuthInterceptor.java b/src/main/java/com/ycwl/basic/interceptor/AuthInterceptor.java index 16d82e1..9ac3312 100644 --- a/src/main/java/com/ycwl/basic/interceptor/AuthInterceptor.java +++ b/src/main/java/com/ycwl/basic/interceptor/AuthInterceptor.java @@ -66,7 +66,7 @@ public class AuthInterceptor extends HandlerInterceptorAdapter { // 获取 token String token = getToken(request); if (StringUtils.isEmpty(token)) { - log.error("==> 请求 header 缺少 Token [{}]", token); + log.error("==> 请求 header 缺少 Token [{}], URL [{}]", token, request.getRequestURL()); throw new MissTokenException("请求头缺少token"); } diff --git a/src/main/java/com/ycwl/basic/mapper/FaceMapper.java b/src/main/java/com/ycwl/basic/mapper/FaceMapper.java index f61dbd3..7fd1c2d 100644 --- a/src/main/java/com/ycwl/basic/mapper/FaceMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/FaceMapper.java @@ -24,11 +24,12 @@ public interface FaceMapper { int deleteByIds(@Param("list") List ids); int update(FaceEntity face); - FaceRespVO getByMemberId(@Param("userId") Long userId, @Param("scenicId") Long scenicId); + FaceRespVO getLatestByMemberId(@Param("userId") Long userId, @Param("scenicId") Long scenicId); int finishedJourney(Long faceId); FaceRespVO findLastFaceByUserId(String userId); + FaceRespVO findLastFaceByScenicAndUserId(Long scenicId, Long userId); List listByScenicAndUserId(String scenicId, Long userId); } diff --git a/src/main/java/com/ycwl/basic/mapper/FaceSampleMapper.java b/src/main/java/com/ycwl/basic/mapper/FaceSampleMapper.java index 35e8d81..bf7bc17 100644 --- a/src/main/java/com/ycwl/basic/mapper/FaceSampleMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/FaceSampleMapper.java @@ -26,5 +26,5 @@ public interface FaceSampleMapper { List listByIds(List list); FaceSampleEntity getEntity(Long faceSampleId); - List listEntity(Long scenicId, Date endDate); + List listEntityBeforeDate(Long scenicId, Date endDate); } diff --git a/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java index 1c0eed6..3748344 100644 --- a/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java +++ b/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java @@ -57,4 +57,5 @@ public class DeviceConfigEntity { * 切割时,取人脸后多少秒的视频 */ private BigDecimal cutPost; + private Integer enablePreBook; } diff --git a/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/entity/FaceDetectLog.java b/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/entity/FaceDetectLog.java index 4aada80..5e9b4dc 100644 --- a/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/entity/FaceDetectLog.java +++ b/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/entity/FaceDetectLog.java @@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject; import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest; import com.aliyuncs.facebody.model.v20191230.SearchFaceResponse; 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 com.ycwl.basic.model.pc.faceDetectLog.resp.MatchLocalRecord; @@ -31,6 +32,9 @@ public class FaceDetectLog { private String matchLocalRecord; + @TableField(exist = false) + private List matchRawRecord; + public static FaceDetectLog quickCreate(String reason) { FaceDetectLog log = new FaceDetectLog(); log.reason = reason; diff --git a/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/resp/MatchLocalRecord.java b/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/resp/MatchLocalRecord.java index c50e368..afd520f 100644 --- a/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/resp/MatchLocalRecord.java +++ b/src/main/java/com/ycwl/basic/model/pc/faceDetectLog/resp/MatchLocalRecord.java @@ -7,6 +7,7 @@ import java.util.Date; @Data public class MatchLocalRecord { private Long faceSampleId; + private String deviceName; private String faceUrl; private Float score; private Float confidence; diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java index 316e810..372e6d4 100644 --- a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java +++ b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java @@ -43,6 +43,7 @@ public class ScenicConfigEntity { * 预约流程,1-预约,2-在线,3-全部 */ private Integer bookRoutine; + private Integer forceFinishTime; /** * 样本保存时间 */ diff --git a/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java b/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java index 70546c4..f975c75 100644 --- a/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java +++ b/src/main/java/com/ycwl/basic/ratelimiter/FixedRateLimiter.java @@ -5,28 +5,36 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -public class FixedRateLimiter { +public class FixedRateLimiter implements IRateLimiter { private final Semaphore semaphore = new Semaphore(1); private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); public FixedRateLimiter(int rate, TimeUnit timeUnit) { // 启动一个线程每0.5秒释放一个许可 scheduler.scheduleAtFixedRate(() -> { - synchronized (semaphore) { - if (semaphore.availablePermits() < 1) { - semaphore.release(1); - } + if (semaphore.availablePermits() < 1) { + semaphore.release(1); } }, rate, rate, timeUnit); } + @Override public void acquire() throws InterruptedException { - synchronized (semaphore) { - semaphore.acquire(); - } + semaphore.acquire(); } + @Override public void shutdown() { scheduler.shutdown(); } + + @Override + public boolean tryAcquire() { + return semaphore.tryAcquire(); + } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + return semaphore.tryAcquire(timeout, unit); + } } diff --git a/src/main/java/com/ycwl/basic/ratelimiter/IRateLimiter.java b/src/main/java/com/ycwl/basic/ratelimiter/IRateLimiter.java new file mode 100644 index 0000000..bdcbbe3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/ratelimiter/IRateLimiter.java @@ -0,0 +1,10 @@ +package com.ycwl.basic.ratelimiter; + +import java.util.concurrent.TimeUnit; + +public interface IRateLimiter { + void acquire() throws InterruptedException; + void shutdown(); + boolean tryAcquire(); + boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException; +} diff --git a/src/main/java/com/ycwl/basic/ratelimiter/SlidingWindowRateLimiter.java b/src/main/java/com/ycwl/basic/ratelimiter/SlidingWindowRateLimiter.java index e815ee3..99571f5 100644 --- a/src/main/java/com/ycwl/basic/ratelimiter/SlidingWindowRateLimiter.java +++ b/src/main/java/com/ycwl/basic/ratelimiter/SlidingWindowRateLimiter.java @@ -6,25 +6,45 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -public class SlidingWindowRateLimiter { +public class SlidingWindowRateLimiter implements IRateLimiter { private final Semaphore semaphore; private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1); public SlidingWindowRateLimiter(int maxRequestsPerSecond) { this.semaphore = new Semaphore(maxRequestsPerSecond); - int rate = 1000000 / maxRequestsPerSecond; scheduler.scheduleAtFixedRate(() -> { if (semaphore.availablePermits() < maxRequestsPerSecond) { - semaphore.release(1); + semaphore.release(maxRequestsPerSecond - semaphore.availablePermits()); } - }, rate, rate, TimeUnit.MICROSECONDS); + }, 1, 1, TimeUnit.SECONDS); } + public SlidingWindowRateLimiter(int maxRequests, int perSecond) { + this.semaphore = new Semaphore(maxRequests); + scheduler.scheduleAtFixedRate(() -> { + if (semaphore.availablePermits() < maxRequests) { + semaphore.release(maxRequests - semaphore.availablePermits()); + } + }, perSecond, perSecond, TimeUnit.SECONDS); + } + + @Override public void acquire() throws InterruptedException { semaphore.acquire(); } + @Override public void shutdown() { scheduler.shutdown(); } + + @Override + public boolean tryAcquire() { + return semaphore.tryAcquire(); + } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + return semaphore.tryAcquire(timeout, unit); + } } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/service/impl/mobile/GoodsServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/mobile/GoodsServiceImpl.java index 8864165..c6ec3c5 100644 --- a/src/main/java/com/ycwl/basic/service/impl/mobile/GoodsServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/impl/mobile/GoodsServiceImpl.java @@ -346,7 +346,7 @@ public class GoodsServiceImpl implements GoodsService { @Override public VideoTaskStatusVO getTaskStatusByScenicId(Long userId, Long scenicId) { - FaceRespVO faceVO = faceMapper.getByMemberId(userId, scenicId); + FaceRespVO faceVO = faceMapper.getLatestByMemberId(userId, scenicId); VideoTaskStatusVO response = new VideoTaskStatusVO(); response.setScenicId(scenicId); if (faceVO == null) { diff --git a/src/main/java/com/ycwl/basic/service/impl/pc/OrderServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/pc/OrderServiceImpl.java index 3ad639d..fb69173 100644 --- a/src/main/java/com/ycwl/basic/service/impl/pc/OrderServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/impl/pc/OrderServiceImpl.java @@ -326,14 +326,6 @@ public class OrderServiceImpl implements OrderService { return ApiResponse.fail("订单添加失败"); } //点击支付按钮统计 - OrderRespVO orderRespVO = orderMapper.getById(orderId); - StatisticsRecordAddReq statisticsRecordAddReq = new StatisticsRecordAddReq(); - statisticsRecordAddReq.setMemberId(orderRespVO.getMemberId()); - statisticsRecordAddReq.setType(StatisticEnum.CLICK_ON_PAYMENT.code); - statisticsRecordAddReq.setScenicId(orderRespVO.getScenicId()); - statisticsRecordAddReq.setMorphId(orderId); - statisticsMapper.addStatisticsRecord(statisticsRecordAddReq); - WxPayRespVO wxPayRespVO = initiatePayment(order, orderItems); return ApiResponse.success(wxPayRespVO); diff --git a/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java index 0c180cf..2c76675 100644 --- a/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/task/impl/TaskFaceServiceImpl.java @@ -1,9 +1,7 @@ package com.ycwl.basic.service.task.impl; -import cn.hutool.core.date.DateUtil; import com.alibaba.fastjson.JSON; import com.aliyuncs.exceptions.ClientException; -import com.aliyuncs.exceptions.ServerException; import com.aliyuncs.facebody.model.v20191230.AddFaceEntityRequest; import com.aliyuncs.facebody.model.v20191230.AddFaceRequest; import com.aliyuncs.facebody.model.v20191230.AddFaceResponse; @@ -16,7 +14,6 @@ import com.aliyuncs.facebody.model.v20191230.ListFaceEntitiesRequest; import com.aliyuncs.facebody.model.v20191230.ListFaceEntitiesResponse; import com.aliyuncs.facebody.model.v20191230.SearchFaceRequest; import com.aliyuncs.facebody.model.v20191230.SearchFaceResponse; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ycwl.basic.biz.OrderBiz; import com.ycwl.basic.config.FaceDetectConfig; import com.ycwl.basic.constant.FaceConstant; @@ -27,22 +24,20 @@ import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; +import com.ycwl.basic.model.pc.device.entity.DeviceEntity; import com.ycwl.basic.model.pc.face.entity.FaceEntity; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; import com.ycwl.basic.model.pc.faceDetectLog.entity.FaceDetectLog; import com.ycwl.basic.model.pc.faceDetectLog.resp.MatchLocalRecord; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; -import com.ycwl.basic.model.pc.faceSample.req.FaceSampleReqQuery; import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; -import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; -import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.model.task.resp.AddFaceSampleRespVo; import com.ycwl.basic.model.task.resp.SearchFaceRespVo; import com.ycwl.basic.ratelimiter.FixedRateLimiter; -import com.ycwl.basic.ratelimiter.SlidingWindowRateLimiter; +import com.ycwl.basic.repository.DeviceRepository; import com.ycwl.basic.repository.FaceRepository; import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.task.TaskFaceService; @@ -55,7 +50,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; @@ -103,6 +97,8 @@ public class TaskFaceServiceImpl implements TaskFaceService { @Autowired private ScenicRepository scenicRepository; + @Autowired + private DeviceRepository deviceRepository; private IAcsClient getClient() { DefaultProfile profile = DefaultProfile.getProfile( @@ -195,29 +191,13 @@ public class TaskFaceServiceImpl implements TaskFaceService { if (matchList.get(0).getFaceItems().isEmpty()) { return respVo; } - List records = matchList.get(0).getFaceItems().stream() - .map(item -> { - MatchLocalRecord record = new MatchLocalRecord(); - record.setIdStr(item.getExtraData()); - record.setFaceSampleId(Long.parseLong(item.getExtraData())); - if (StringUtils.isNumeric(item.getDbName())) { - FaceSampleEntity faceSample = faceRepository.getFaceSample(record.getFaceSampleId()); - if (faceSample != null) { - record.setFaceUrl(faceSample.getFaceUrl()); - record.setShotDate(faceSample.getCreateAt()); - } - } else { - record.setFaceUrl(getFaceUrl(record.getFaceSampleId())); - } - record.setScore(item.getScore()); - record.setConfidence(item.getConfidence()); - return record; - }) - .collect(Collectors.toList()); - log.matchLocalRecord(records); + List records = matchList.get(0).getFaceItems(); + log.setMatchRawRecord(records); List faceSampleIds = records.stream() .filter(record -> record.getScore() > 0.525F) - .map(MatchLocalRecord::getFaceSampleId) + .map(SearchFaceResponse.Data.MatchListItem.FaceItemsItem::getExtraData) + .filter(StringUtils::isNumeric) + .map(Long::valueOf) .collect(Collectors.toList()); respVo.setFirstMatchRate(matchList.get(0).getFaceItems().get(0).getScore()); respVo.setSampleListIds(faceSampleIds); @@ -227,7 +207,33 @@ public class TaskFaceServiceImpl implements TaskFaceService { e.printStackTrace(); throw new BaseException(e.getMessage()); } finally { - logMapper.insert(log); + new Thread(() -> { + if (log.getMatchRawRecord() != null) { + List collect = log.getMatchRawRecord().parallelStream().map(item -> { + MatchLocalRecord record = new MatchLocalRecord(); + record.setIdStr(item.getExtraData()); + record.setFaceSampleId(Long.parseLong(item.getExtraData())); + if (StringUtils.isNumeric(item.getDbName())) { + FaceSampleEntity faceSample = faceRepository.getFaceSample(record.getFaceSampleId()); + if (faceSample != null) { + DeviceEntity device = deviceRepository.getDevice(faceSample.getDeviceId()); + if (device != null) { + record.setDeviceName(device.getName()); + } + record.setFaceUrl(faceSample.getFaceUrl()); + record.setShotDate(faceSample.getCreateAt()); + } + } else { + record.setFaceUrl(getFaceUrl(record.getFaceSampleId())); + } + record.setScore(item.getScore()); + record.setConfidence(item.getConfidence()); + return record; + }).collect(Collectors.toList()); + log.setMatchLocalRecord(JSON.toJSONString(collect)); + } + logMapper.insert(log); + }).start(); } } @@ -289,20 +295,6 @@ public class TaskFaceServiceImpl implements TaskFaceService { sampleStoreDay = 7; } Date endDate = DateUtils.addDateDays(new Date(), -(sampleStoreDay + 1)); - List faceSampleList = faceSampleMapper.listEntity(scenicId, endDate); - if (faceSampleList.isEmpty()) { - log.info("当前景区{},人脸样本为空", scenicId); - return; - } - faceSampleList.forEach(faceSample -> { - boolean success = deleteFaceSample(String.valueOf(scenicId), generateEntityId(faceSample)); - if (success) { - log.info("当前景区{},人脸样本ID{},删除成功", scenicId, faceSample.getId()); - faceSampleMapper.deleteById(faceSample.getId()); - } else { - log.info("当前景区{},人脸样本ID{},删除失败", scenicId, faceSample.getId()); - } - }); ListFaceEntitiesRequest listFaceEntitiesRequest = new ListFaceEntitiesRequest(); listFaceEntitiesRequest.setDbName(String.valueOf(scenicId)); listFaceEntitiesRequest.setOrder("asc"); @@ -342,6 +334,20 @@ public class TaskFaceServiceImpl implements TaskFaceService { } } catch (Exception ignored) { } + List faceSampleList = faceSampleMapper.listEntityBeforeDate(scenicId, endDate); + if (faceSampleList.isEmpty()) { + log.info("当前景区{},人脸样本为空", scenicId); + return; + } + faceSampleList.forEach(faceSample -> { + boolean success = deleteFaceSample(String.valueOf(scenicId), generateEntityId(faceSample)); + if (success) { + log.info("当前景区{},人脸样本ID{},删除成功", scenicId, faceSample.getId()); + faceSampleMapper.deleteById(faceSample.getId()); + } else { + log.info("当前景区{},人脸样本ID{},删除失败", scenicId, faceSample.getId()); + } + }); } @Override diff --git a/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java b/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java index 4f4a4a1..5c12801 100644 --- a/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/task/impl/TaskTaskServiceImpl.java @@ -345,7 +345,8 @@ public class TaskTaskServiceImpl implements TaskService { if (templateList == null || templateList.isEmpty()) { return; } - if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine())) { + if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine()) || Integer.valueOf(4).equals(scenicConfig.getBookRoutine())) { + // 生成全部视频的逻辑 templateList.forEach(template -> { createTaskByFaceIdAndTempalteId(faceId, template.getId(), 1); }); diff --git a/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java index 8c97dda..424e84c 100644 --- a/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java +++ b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java @@ -90,7 +90,7 @@ public class DynamicTaskGenerator { queue.add(new Task(faceSampleId, createTime)); } - @Scheduled(fixedDelay = 500L) + @Scheduled(fixedDelay = 1000L) public void doTask() { Task task = queue.poll(); if (task == null) { @@ -111,7 +111,7 @@ public class DynamicTaskGenerator { log.debug("当前景区{},无配置", faceSample.getScenicId()); return; } - if (!Integer.valueOf(1).equals(scenicConfig.getBookRoutine()) && !Integer.valueOf(3).equals(scenicConfig.getBookRoutine())) { + if (!Integer.valueOf(5).equals(scenicConfig.getBookRoutine())) { log.debug("当前景区{}未启用预约流程,跳过", faceSample.getScenicId()); return; } diff --git a/src/main/java/com/ycwl/basic/task/FaceCleaner.java b/src/main/java/com/ycwl/basic/task/FaceCleaner.java index 30102f7..1bd68b9 100644 --- a/src/main/java/com/ycwl/basic/task/FaceCleaner.java +++ b/src/main/java/com/ycwl/basic/task/FaceCleaner.java @@ -3,10 +3,15 @@ package com.ycwl.basic.task; import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.SourceMapper; +import com.ycwl.basic.mapper.VideoMapper; import com.ycwl.basic.model.pc.faceSample.req.FaceSampleReqQuery; import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; +import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; +import com.ycwl.basic.model.pc.video.req.VideoReqQuery; +import com.ycwl.basic.model.pc.video.resp.VideoRespVO; +import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.service.task.TaskFaceService; import com.ycwl.basic.storage.StorageFactory; import com.ycwl.basic.storage.adapters.IStorageAdapter; @@ -31,6 +36,10 @@ public class FaceCleaner { private FaceSampleMapper faceSampleMapper; @Autowired private SourceMapper sourceMapper; + @Autowired + private VideoMapper videoMapper; + @Autowired + private ScenicRepository scenicRepository; @Scheduled(cron = "0 0 4 * * ?") public void clean(){ @@ -44,11 +53,43 @@ public class FaceCleaner { @Scheduled(cron = "0 0 3 * * ?") public void deleteExpiredSource(){ - + ScenicReqQuery scenicQuery = new ScenicReqQuery(); + List scenicList = scenicMapper.list(scenicQuery); + scenicList.parallelStream().forEach(scenic -> { + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenic.getId()); + if (scenicConfig == null) { + log.info("当前景区{},无配置信息", scenic.getName()); + return; + } + int imageSourceExpireDay = 7; + int videoSourceExpireDay = 7; + if (scenicConfig.getImageSourceStoreDay() != null) { + imageSourceExpireDay = scenicConfig.getImageSourceStoreDay(); + } else { + log.info("当前景区{},原始素材保存天数未设置,默认7天", scenic.getName()); + } + if (scenicConfig.getVideoSourceStoreDay() != null) { + videoSourceExpireDay = scenicConfig.getVideoSourceStoreDay(); + } else { + log.info("当前景区{},原始素材保存天数未设置,默认7天", scenic.getName()); + } + if (Integer.valueOf(1).equals(scenicConfig.getDisableSourceVideo())) { + return; + } + if (Integer.valueOf(1).equals(scenicConfig.getDisableSourceImage())) { + return; + } + log.info("当前景区{},开始删除原始素材", scenic.getName()); + }); } @Scheduled(cron = "0 0 5 * * ?") - public void clear(){ + public void clearOss(){ + cleanFaceSampleOss(); + cleanSourceOss(); + cleanVideoOss(); + } + private void cleanFaceSampleOss() { log.info("开始清理人脸文件"); List faceSampleRespVOS = faceSampleMapper.list(new FaceSampleReqQuery()); IStorageAdapter adapter = StorageFactory.use("faces"); @@ -60,4 +101,20 @@ public class FaceCleaner { } }); } + private void cleanSourceOss() { + } + private void cleanVideoOss() { + log.info("开始清理视频文件"); + List videoRespVOS = videoMapper.list(new VideoReqQuery()); + IStorageAdapter adapter = StorageFactory.use("video"); + List fileObjectList = adapter.listDir(""); + fileObjectList.parallelStream().forEach(fileObject -> { + if (videoRespVOS.parallelStream().noneMatch(videoRespVO -> videoRespVO.getVideoUrl().contains(fileObject.getFullPath()))){ + log.info("删除视频文件:{}", fileObject); + adapter.deleteFile(fileObject.getFullPath()); + } else { + log.info("视频文件存在关系:{},未删除", fileObject); + } + }); + } } diff --git a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java index ed41f67..a2db559 100644 --- a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java +++ b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java @@ -1,18 +1,17 @@ package com.ycwl.basic.task; import com.ycwl.basic.biz.OrderBiz; +import com.ycwl.basic.biz.TaskStatusBiz; import com.ycwl.basic.device.DeviceFactory; import com.ycwl.basic.device.entity.common.FileObject; import com.ycwl.basic.device.operator.IDeviceStorageOperator; import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity; import com.ycwl.basic.repository.DeviceRepository; -import com.ycwl.basic.mapper.DeviceMapper; import com.ycwl.basic.mapper.FaceSampleMapper; import com.ycwl.basic.mapper.SourceMapper; import com.ycwl.basic.model.mobile.order.IsBuyRespVO; import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity; import com.ycwl.basic.model.pc.device.entity.DeviceEntity; -import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO; import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity; import com.ycwl.basic.model.pc.source.entity.SourceEntity; import com.ycwl.basic.repository.TemplateRepository; @@ -34,7 +33,6 @@ import java.io.InputStreamReader; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; @@ -57,6 +55,8 @@ public class VideoPieceGetter { private OrderBiz orderBiz; @Autowired private TemplateRepository templateRepository; + @Autowired + private TaskStatusBiz taskStatusBiz; @Data public static class Task { @@ -97,6 +97,9 @@ public class VideoPieceGetter { } else { templatePlaceholder = null; } + if (task.faceId != null) { + taskStatusBiz.setFaceCutStatus(task.faceId, 0); + } AtomicBoolean invoke = new AtomicBoolean(false); List currentPlaceholder = new ArrayList<>(); List list = faceSampleMapper.listByIds(task.getFaceSampleIds()); @@ -113,7 +116,7 @@ public class VideoPieceGetter { .stream() .parallel() .forEach(faceSampleList -> { - faceSampleList.forEach(faceSample -> { + faceSampleList.parallelStream().forEach(faceSample -> { DeviceEntity device = deviceRepository.getDevice(faceSample.getDeviceId()); DeviceConfigEntity config = deviceRepository.getDeviceConfig(faceSample.getDeviceId()); @@ -245,6 +248,9 @@ public class VideoPieceGetter { } }); }); + if (task.faceId != null) { + taskStatusBiz.setFaceCutStatus(task.faceId, 1); + } if (null != task.getCallback()) { if (!invoke.get()) { invoke.set(true); diff --git a/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java b/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java index c0911e6..bd73c15 100644 --- a/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java +++ b/src/main/java/com/ycwl/basic/task/VideoTaskGenerator.java @@ -4,10 +4,14 @@ import cn.hutool.core.date.DateUtil; import com.ycwl.basic.biz.TemplateBiz; import com.ycwl.basic.mapper.FaceMapper; import com.ycwl.basic.mapper.FaceSampleMapper; +import com.ycwl.basic.mapper.ScenicMapper; import com.ycwl.basic.mapper.TemplateMapper; import com.ycwl.basic.model.mobile.scenic.content.ContentPageVO; import com.ycwl.basic.model.pc.face.req.FaceReqQuery; import com.ycwl.basic.model.pc.face.resp.FaceRespVO; +import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity; +import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery; +import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO; import com.ycwl.basic.model.task.resp.SearchFaceRespVo; import com.ycwl.basic.repository.ScenicRepository; import com.ycwl.basic.repository.TemplateRepository; @@ -20,6 +24,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.util.Calendar; import java.util.Date; import java.util.List; @@ -38,31 +43,71 @@ public class VideoTaskGenerator { private TaskTaskServiceImpl taskTaskService; @Autowired private TemplateMapper templateMapper; + @Autowired + private ScenicMapper scenicMapper; + @Autowired + private ScenicRepository scenicRepository; // TODO: 可配置,现在赶时间暂时写死 - @Scheduled(cron = "0 0 18 * * *") + @Scheduled(cron = "0 0 * * * *") public void generateVideoTask() { - // 指定,获取指定日期的未完成人脸样本,并生成任务 - Long scenicId = 3946669713328836608L; - List contentList = templateMapper.listFor(scenicId); - if (contentList.isEmpty()) { + List scenicList = scenicMapper.list(new ScenicReqQuery()); + if (scenicList.isEmpty()) { return; } - Long templateId = contentList.get(0).getTemplateId(); - FaceReqQuery query = new FaceReqQuery(); - query.setScenicId(scenicId); - query.setStartTime(DateUtil.beginOfDay(new Date())); - query.setEndTime(DateUtil.endOfDay(new Date())); - List list = faceMapper.list(query); - list.stream().parallel().forEach(face -> { - taskFaceService.searchFace(face.getId()); - boolean canAutoGenerate = templateBiz.determineTemplateCanAutoGenerate(templateId, face.getId(), false); - if (canAutoGenerate) { - log.info("task callback: 自动生成"); - taskTaskService.forceCreateTaskByFaceIdAndTempalteId(face.getId(), templateId); - } else { - log.info("task callback: 不自动生成"); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + int currentHour = calendar.get(Calendar.HOUR_OF_DAY); + calendar.clear(); + scenicList.parallelStream().forEach(scenic -> { + Long scenicId = scenic.getId(); + ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(scenicId); + if (scenicConfig == null) { + log.info("当前景区{},无配置信息", scenic.getName()); + return; + } + if (Integer.valueOf(1).equals(scenicConfig.getBookRoutine()) || Integer.valueOf(3).equals(scenicConfig.getBookRoutine())) { + Integer hour = scenicConfig.getForceFinishTime(); + if (hour != currentHour) { + return; + } + // 定时逻辑 + List contentList = templateMapper.listFor(scenicId); + if (contentList.isEmpty()) { + return; + } + FaceReqQuery query = new FaceReqQuery(); + query.setScenicId(scenicId); + query.setStartTime(DateUtil.beginOfDay(new Date())); + query.setEndTime(DateUtil.endOfDay(new Date())); + List list = faceMapper.list(query); + list.stream().parallel().forEach(face -> { + taskFaceService.searchFace(face.getId()); + if (Integer.valueOf(3).equals(scenicConfig.getBookRoutine())) { + // 全部生成 + contentList.forEach(content -> { + Long templateId = content.getTemplateId(); + boolean canAutoGenerate = templateBiz.determineTemplateCanAutoGenerate(templateId, face.getId(), false); + if (canAutoGenerate) { + log.info("task callback: 自动生成"); + taskTaskService.forceCreateTaskByFaceIdAndTempalteId(face.getId(), templateId); + } else { + log.info("task callback: 不自动生成"); + } + }); + } else { + Long templateId = contentList.get(0).getTemplateId(); + boolean canAutoGenerate = templateBiz.determineTemplateCanAutoGenerate(templateId, face.getId(), false); + if (canAutoGenerate) { + log.info("task callback: 自动生成"); + taskTaskService.forceCreateTaskByFaceIdAndTempalteId(face.getId(), templateId); + } else { + log.info("task callback: 不自动生成"); + } + } + }); } }); } diff --git a/src/main/resources/mapper/DeviceMapper.xml b/src/main/resources/mapper/DeviceMapper.xml index 7870c93..1f79f0b 100644 --- a/src/main/resources/mapper/DeviceMapper.xml +++ b/src/main/resources/mapper/DeviceMapper.xml @@ -36,7 +36,8 @@ online_check = #{onlineCheck}, online_max_interval = #{onlineMaxInterval}, cut_pre = #{cutPre}, - cut_post = #{cutPost} + cut_post = #{cutPost}, + enable_pre_book = #{enablePreBook} where id = #{id} diff --git a/src/main/resources/mapper/FaceMapper.xml b/src/main/resources/mapper/FaceMapper.xml index a86fe57..38f5b85 100644 --- a/src/main/resources/mapper/FaceMapper.xml +++ b/src/main/resources/mapper/FaceMapper.xml @@ -70,7 +70,7 @@ from face where id = #{id} - select id, scenic_id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result, create_at, update_at from face where member_id = #{userId} and scenic_id = #{scenicId} @@ -100,4 +100,11 @@ where member_id = #{userId} and scenic_id = #{scenicId} order by update_at desc + diff --git a/src/main/resources/mapper/FaceSampleMapper.xml b/src/main/resources/mapper/FaceSampleMapper.xml index 71722db..4884d10 100644 --- a/src/main/resources/mapper/FaceSampleMapper.xml +++ b/src/main/resources/mapper/FaceSampleMapper.xml @@ -101,7 +101,7 @@ from face_sample where id = #{id} - select * from face_sample where scenic_id = #{scenicId} and create_at <= #{endDate} diff --git a/src/main/resources/mapper/ScenicMapper.xml b/src/main/resources/mapper/ScenicMapper.xml index 7305cb1..7d02acd 100644 --- a/src/main/resources/mapper/ScenicMapper.xml +++ b/src/main/resources/mapper/ScenicMapper.xml @@ -94,7 +94,8 @@ disable_source_video=#{disableSourceVideo}, disable_source_image=#{disableSourceImage}, video_source_store_day=#{videoSourceStoreDay}, - image_source_store_day=#{imageSourceStoreDay} + image_source_store_day=#{imageSourceStoreDay}, + force_finish_time=#{forceFinishTime} where id = #{id}