Commit 2527b568c7bf386cd4dfa21cd1c203ffa92b265f

Authored by 王彪总
1 parent e0c2df32

feat(inspection): 添加巡检二维码打卡功能支持

- 新增 ServiceCmdEventPublishing 导入实现CMD注册表fallback机制
- 修改鉴权逻辑支持Redis缓存未命中时从CMD注册表验证服务权限
- 实现二维码巡检打卡接口无需登录认证的访问控制
- 更新巡检任务详情接口支持通过员工编号查询并返回完整打卡信息
- 添加GPS坐标校验确保打卡位置有效性
- 实现二维码打卡距离校验防止远程作弊(限制2公里内)
- 集成考勤记录同步功能使打卡数据在考勤页面可见
- 优化CMD调度支持服务未缓存时的直接命令处理机制
service-api/src/main/java/com/java110/api/configuration/ServiceConfiguration.java
... ... @@ -142,7 +142,11 @@ public class ServiceConfiguration {
142 142  
143 143  
144 144  
145   - exclusions.append("/app/reportInfoAnswer/queryReportInfoAnswerByOpenId");
  145 + exclusions.append("/app/reportInfoAnswer/queryReportInfoAnswerByOpenId,");
  146 + // 二维码巡检打卡接口(无需登录)
  147 + exclusions.append("/app/inspection.queryStaffTodayTasks,");
  148 + exclusions.append("/app/inspectionTaskDetail.updateInspectionTaskDetail,");
  149 + exclusions.append("/callComponent/upload/uploadFile/uploadImage,");
146 150  
147 151  
148 152 final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
... ...
service-api/src/main/java/com/java110/api/smo/impl/ApiServiceSMOImpl.java
... ... @@ -8,6 +8,7 @@ import com.java110.core.client.RestTemplate;
8 8 import com.java110.core.context.ApiDataFlow;
9 9 import com.java110.core.context.DataFlow;
10 10 import com.java110.core.context.Environment;
  11 +import com.java110.core.event.cmd.ServiceCmdEventPublishing;
11 12 import com.java110.core.event.service.api.ServiceDataFlowEventPublishing;
12 13 import com.java110.core.factory.AuthenticationFactory;
13 14 import com.java110.core.factory.DataFlowFactory;
... ... @@ -339,13 +340,18 @@ public class ApiServiceSMOImpl extends LoggerEngine implements IApiServiceSMO {
339 340  
340 341  
341 342 //todo 判断 AppId 是否有权限操作相应的服务
342   - AppService appService = DataFlowFactory.getService(dataFlow, dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE));
  343 + String serviceCode = dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE);
  344 + AppService appService = DataFlowFactory.getService(dataFlow, serviceCode);
343 345  
344   - //这里调用缓存 查询缓存信息
  346 + //这里调用缓存 查询缓存信息,如果缓存中没有则fallback到Cmd扫描注册表
345 347 if (appService == null || !CommonConstant.HTTP_SERVICE_API.equals(appService.getBusinessTypeCd())) {
346   - //添加耗时
347   - DataFlowFactory.addCostTime(dataFlow, "judgeAuthority", "鉴权耗时", startDate);
348   - throw new NoAuthorityException(ResponseConstant.RESULT_CODE_NO_AUTHORITY_ERROR, "AppId 没有权限访问 serviceCode = " + dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE));
  348 + // fallback: 检查是否为通过@Java110Cmd注解注册的服务(代码级注册,可能未同步到Redis)
  349 + if (ServiceCmdEventPublishing.getListeners(serviceCode).size() > 0) {
  350 + logger.debug("服务 {} 从Cmd注册表验证通过(Redis缓存未命中或businessType不匹配)", serviceCode);
  351 + } else {
  352 + DataFlowFactory.addCostTime(dataFlow, "judgeAuthority", "鉴权耗时", startDate);
  353 + throw new NoAuthorityException(ResponseConstant.RESULT_CODE_NO_AUTHORITY_ERROR, "AppId 没有权限访问 serviceCode = " + serviceCode);
  354 + }
349 355 }
350 356  
351 357  
... ... @@ -379,6 +385,16 @@ public class ApiServiceSMOImpl extends LoggerEngine implements IApiServiceSMO {
379 385 Date startDate = DateUtil.getCurrentDate();
380 386 //todo 拿到当前服务
381 387 AppService appService = DataFlowFactory.getService(dataFlow, dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE));
  388 + if (appService == null) {
  389 + // fallback: Redis未缓存但在Cmd注册表中的服务,直接CMD调度
  390 + String svc = dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE);
  391 + if (ServiceCmdEventPublishing.getListeners(svc).size() > 0) {
  392 + dealCmd(null, dataFlow, dataFlow.getReqJson());
  393 + DataFlowFactory.addCostTime(dataFlow, "invokeBusinessSystem", "调用下游系统耗时", startDate);
  394 + return;
  395 + }
  396 + throw new BusinessException(ResponseConstant.RESULT_CODE_ERROR, "服务不存在:" + svc);
  397 + }
382 398 //todo 这里对透传类处理,目前很少用到,可以不用关注
383 399 if ("NT".equals(appService.getIsInstance())) {
384 400 //如果是透传类 请求方式必须与接口提供方调用方式一致
... ... @@ -399,10 +415,9 @@ public class ApiServiceSMOImpl extends LoggerEngine implements IApiServiceSMO {
399 415 }
400 416 doTransfer(appService, dataFlow, dataFlow.getReqJson());
401 417 return;
402   - } else if ("CMD".equals(appService.getIsInstance())) { // todo 微服务调用方式,目前主要用这种方式调度分发 到不同的微服务,这里是通过c_service 中配置 调用到不同的微服务
403   - //如果是透传类 请求方式必须与接口提供方调用方式一致
  418 + } else if ("CMD".equals(appService.getIsInstance())) { // todo 微服务调用方式
404 419 String httpMethod = dataFlow.getRequestCurrentHeaders().get(CommonConstant.HTTP_METHOD);
405   - if (!appService.getMethod().equals(httpMethod)) {
  420 + if (!StringUtils.isEmpty(appService.getMethod()) && !appService.getMethod().equals(httpMethod)) {
406 421 throw new ListenerExecuteException(ResponseConstant.RESULT_CODE_ERROR,
407 422 "服务【" + appService.getServiceCode() + "】调用方式不对请检查,当前请求方式为:" + httpMethod);
408 423 }
... ... @@ -574,6 +589,13 @@ public class ApiServiceSMOImpl extends LoggerEngine implements IApiServiceSMO {
574 589 * @param reqJson
575 590 */
576 591 private void dealCmd(AppService appService, ApiDataFlow dataFlow, JSONObject reqJson) {
  592 + // fallback: 如果appService为null,用header中的serviceCode创建临时对象
  593 + if (appService == null) {
  594 + appService = new AppService();
  595 + appService.setServiceCode(dataFlow.getRequestHeaders().get(CommonConstant.HTTP_SERVICE));
  596 + appService.setMethod("");
  597 + appService.setIsInstance("CMD");
  598 + }
577 599 Map<String, String> reqHeader = dataFlow.getRequestCurrentHeaders();
578 600 HttpHeaders header = new HttpHeaders();
579 601 //todo 对头信息重新包装
... ...
service-community/src/main/java/com/java110/community/cmd/inspection/QueryStaffTodayTasksCmd.java
... ... @@ -7,8 +7,10 @@ import com.java110.core.event.cmd.Cmd;
7 7 import com.java110.core.event.cmd.CmdEvent;
8 8 import com.java110.dto.inspection.InspectionTaskDetailDto;
9 9 import com.java110.dto.inspection.InspectionTaskDto;
  10 +import com.java110.dto.user.UserDto;
10 11 import com.java110.intf.community.IInspectionTaskDetailV1InnerServiceSMO;
11 12 import com.java110.intf.community.IInspectionTaskV1InnerServiceSMO;
  13 +import com.java110.intf.user.IUserInnerServiceSMO;
12 14 import com.java110.utils.exception.CmdException;
13 15 import com.java110.utils.util.Assert;
14 16 import com.java110.utils.util.BeanConvertUtil;
... ... @@ -20,15 +22,6 @@ import org.springframework.http.ResponseEntity;
20 22 import java.util.ArrayList;
21 23 import java.util.List;
22 24  
23   -/**
24   - * 查询巡检人员今日任务及巡检点
25   - * 用于二维码扫码打卡页面加载
26   - * serviceCode: inspection.queryStaffTodayTasks
27   - *
28   - * @author Java110
29   - * @version 1.0
30   - * @since 2024
31   - */
32 25 @Java110Cmd(serviceCode = "inspection.queryStaffTodayTasks")
33 26 public class QueryStaffTodayTasksCmd extends Cmd {
34 27  
... ... @@ -38,18 +31,30 @@ public class QueryStaffTodayTasksCmd extends Cmd {
38 31 @Autowired
39 32 private IInspectionTaskDetailV1InnerServiceSMO inspectionTaskDetailV1InnerServiceSMOImpl;
40 33  
  34 + @Autowired
  35 + private IUserInnerServiceSMO userInnerServiceSMOImpl;
  36 +
41 37 @Override
42 38 public void validate(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException {
43   - Assert.hasKeyAndValue(reqJson, "staffId", "请求报文中未包含staffId");
44   - Assert.hasKeyAndValue(reqJson, "communityId", "请求报文中未包含communityId");
  39 + Assert.hasKeyAndValue(reqJson, "staffCode", "请求报文中未包含staffCode");
45 40 }
46 41  
47 42 @Override
48 43 public void doCmd(CmdEvent event, ICmdDataFlowContext context, JSONObject reqJson) throws CmdException {
49   - String staffId = reqJson.getString("staffId");
50   - String communityId = reqJson.getString("communityId");
  44 + String staffCode = reqJson.getString("staffCode");
  45 + String communityId = reqJson.getString("communityId") != null ? reqJson.getString("communityId") : "";
  46 +
  47 + // 根据员工编号查询用户
  48 + UserDto userDto = new UserDto();
  49 + userDto.setName(staffCode);
  50 + List<UserDto> users = userInnerServiceSMOImpl.getUsers(userDto);
  51 + String staffId = "-1";
  52 + String staffName = staffCode;
  53 + if (users != null && users.size() > 0) {
  54 + staffId = users.get(0).getUserId();
  55 + staffName = users.get(0).getName();
  56 + }
51 57  
52   - // 查询该巡检人员的巡检任务
53 58 InspectionTaskDto inspectionTaskDto = new InspectionTaskDto();
54 59 inspectionTaskDto.setPlanUserId(staffId);
55 60 inspectionTaskDto.setCommunityId(communityId);
... ... @@ -57,24 +62,22 @@ public class QueryStaffTodayTasksCmd extends Cmd {
57 62  
58 63 JSONObject result = new JSONObject();
59 64 result.put("staffId", staffId);
  65 + result.put("staffName", staffName);
  66 + result.put("staffCode", staffCode);
  67 + result.put("communityId", communityId);
60 68  
61 69 List<JSONObject> taskList = new ArrayList<>();
62   - String staffName = "";
63 70 for (InspectionTaskDto task : tasks) {
64   - if (staffName.isEmpty() && task.getPlanUserName() != null) {
65   - staffName = task.getPlanUserName();
66   - }
67   -
68 71 InspectionTaskDetailDto detailDto = new InspectionTaskDetailDto();
69 72 detailDto.setTaskId(task.getTaskId());
70 73 detailDto.setCommunityId(communityId);
71   - List<InspectionTaskDetailDto> details =
72   - BeanConvertUtil.covertBeanList(
73   - inspectionTaskDetailV1InnerServiceSMOImpl.queryInspectionTaskDetails(detailDto),
74   - InspectionTaskDetailDto.class);
  74 + List<InspectionTaskDetailDto> details = BeanConvertUtil.covertBeanList(
  75 + inspectionTaskDetailV1InnerServiceSMOImpl.queryInspectionTaskDetails(detailDto),
  76 + InspectionTaskDetailDto.class);
75 77  
76 78 JSONObject taskObj = new JSONObject();
77 79 taskObj.put("taskId", task.getTaskId());
  80 + taskObj.put("communityId", communityId);
78 81 taskObj.put("planName", task.getInspectionPlanName());
79 82 taskObj.put("planInsTime", task.getPlanInsTime());
80 83 taskObj.put("planEndTime", task.getPlanEndTime());
... ... @@ -106,11 +109,9 @@ public class QueryStaffTodayTasksCmd extends Cmd {
106 109 }
107 110  
108 111 result.put("staffName", staffName);
109   - result.put("communityId", communityId);
110 112 result.put("tasks", taskList);
111 113  
112   - ResponseEntity<String> responseEntity = new ResponseEntity<>(
113   - ResultVo.createResponseEntity(result).toString(), HttpStatus.OK);
114   - context.setResponseEntity(responseEntity);
  114 + context.setResponseEntity(new ResponseEntity<>(
  115 + ResultVo.createResponseEntity(result).toString(), HttpStatus.OK));
115 116 }
116 117 }
... ...
service-community/src/main/java/com/java110/community/cmd/inspectionTaskDetail/UpdateInspectionTaskDetailCmd.java
... ... @@ -8,6 +8,7 @@ import com.java110.core.context.ICmdDataFlowContext;
8 8 import com.java110.core.event.cmd.Cmd;
9 9 import com.java110.core.event.cmd.CmdEvent;
10 10 import com.java110.core.log.LoggerFactory;
  11 +import com.java110.dto.inspection.InspectionDto;
11 12 import com.java110.dto.inspection.InspectionTaskDetailDto;
12 13 import com.java110.dto.inspection.InspectionTaskDto;
13 14 import com.java110.intf.common.IFileRelInnerServiceSMO;
... ... @@ -15,10 +16,13 @@ import com.java110.intf.community.IInspectionTaskDetailInnerServiceSMO;
15 16 import com.java110.intf.community.IInspectionTaskDetailV1InnerServiceSMO;
16 17 import com.java110.intf.community.IInspectionTaskInnerServiceSMO;
17 18 import com.java110.intf.community.IInspectionTaskV1InnerServiceSMO;
  19 +import com.java110.intf.community.IInspectionPointV1InnerServiceSMO;
18 20 import com.java110.po.file.FileRelPo;
19 21 import com.java110.po.inspection.InspectionTaskDetailPo;
20 22 import com.java110.po.inspection.InspectionTaskPo;
  23 +import com.java110.po.property.AttendanceRecordPo;
21 24 import com.java110.utils.exception.CmdException;
  25 +import com.java110.utils.factory.ApplicationContextFactory;
22 26 import com.java110.utils.util.Assert;
23 27 import com.java110.utils.util.BeanConvertUtil;
24 28 import com.java110.utils.util.DateUtil;
... ... @@ -26,6 +30,7 @@ import com.java110.utils.util.StringUtil;
26 30 import org.slf4j.Logger;
27 31 import org.springframework.beans.factory.annotation.Autowired;
28 32  
  33 +import java.math.BigDecimal;
29 34 import java.text.ParseException;
30 35 import java.text.SimpleDateFormat;
31 36 import java.util.Date;
... ... @@ -59,6 +64,9 @@ public class UpdateInspectionTaskDetailCmd extends Cmd {
59 64 @Autowired
60 65 private IInspectionTaskV1InnerServiceSMO inspectionTaskV1InnerServiceSMOImpl;
61 66  
  67 + @Autowired
  68 + private IInspectionPointV1InnerServiceSMO inspectionPointV1InnerServiceSMOImpl;
  69 +
62 70 /**
63 71 * 参数校验方法
64 72 * 验证请求参数中是否包含必要的字段
... ... @@ -73,8 +81,11 @@ public class UpdateInspectionTaskDetailCmd extends Cmd {
73 81 // 校验必要参数是否存在
74 82 Assert.hasKeyAndValue(reqJson, "taskDetailId", "请求报文中未包含taskDetailId");
75 83 Assert.hasKeyAndValue(reqJson, "taskId", "请求报文中未包含taskId");
76   - Assert.hasKeyAndValue(reqJson, "communityId", "请求报文中未包含communityId");
77 84 Assert.hasKeyAndValue(reqJson, "inspectionId", "请求报文中未包含inspectionId");
  85 + // qrcode打卡时communityId从任务中获取,不强制要求
  86 + if (!"qrcode".equals(reqJson.getString("source"))) {
  87 + Assert.hasKeyAndValue(reqJson, "communityId", "请求报文中未包含communityId");
  88 + }
78 89 Assert.hasKeyAndValue(reqJson, "photos", "请求报文中未包含照片");
79 90 }
80 91  
... ... @@ -95,6 +106,29 @@ public class UpdateInspectionTaskDetailCmd extends Cmd {
95 106 dealPhotos(reqJson);
96 107 }
97 108  
  109 + // GPS校验:所有打卡都必须有有效坐标
  110 + String lng = reqJson.getString("longitude");
  111 + String lat = reqJson.getString("latitude");
  112 + if (StringUtil.isEmpty(lng) || "0".equals(lng) || "0.0".equals(lng) || "0.0000000".equals(lng)
  113 + || StringUtil.isEmpty(lat) || "0".equals(lat) || "0.0".equals(lat) || "0.0000000".equals(lat)) {
  114 + throw new CmdException("请开启GPS获取位置后再打卡");
  115 + }
  116 +
  117 + // 二维码打卡时校验距离(仅H5,不影响App)
  118 + if ("qrcode".equals(reqJson.getString("source"))) {
  119 + // 从任务中获取communityId
  120 + if (StringUtil.isEmpty(reqJson.getString("communityId"))) {
  121 + InspectionTaskDto taskDto = new InspectionTaskDto();
  122 + taskDto.setTaskId(reqJson.getString("taskId"));
  123 + List<InspectionTaskDto> tasks = inspectionTaskInnerServiceSMOImpl.queryInspectionTasks(taskDto);
  124 + if (tasks != null && tasks.size() > 0) {
  125 + reqJson.put("communityId", tasks.get(0).getCommunityId());
  126 + }
  127 + }
  128 + checkQrcodeDistance(reqJson);
  129 + saveQrcodeAttendanceRecord(reqJson);
  130 + }
  131 +
98 132 // 设置巡检点完成状态
99 133 reqJson.put("state", "20200407"); // 巡检点完成状态码
100 134  
... ... @@ -320,10 +354,75 @@ public class UpdateInspectionTaskDetailCmd extends Cmd {
320 354  
321 355 // 执行更新操作
322 356 int flag = inspectionTaskV1InnerServiceSMOImpl.updateInspectionTask(inspectionTaskPo);
323   -
  357 +
324 358 // 检查更新结果
325 359 if (flag < 1) {
326 360 throw new CmdException("修改任务失败");
327 361 }
328 362 }
  363 +
  364 + /**
  365 + * 二维码打卡时同步写入考勤记录(attendance_record),使打卡记录在考勤页面可见
  366 + */
  367 + private void saveQrcodeAttendanceRecord(JSONObject reqJson) {
  368 + try {
  369 + AttendanceRecordPo po = new AttendanceRecordPo();
  370 + po.setId(com.java110.core.factory.GenerateCodeFactory.getGeneratorId("10"));
  371 + po.setUserId(reqJson.getString("userId"));
  372 + po.setUserName(reqJson.getString("userName"));
  373 + po.setPunchType(reqJson.containsKey("punchType") ? reqJson.getString("punchType") : "ON");
  374 + po.setPunchTime(new Date());
  375 + try { po.setLongitude(new BigDecimal(reqJson.getString("longitude"))); } catch(Exception ex) {}
  376 + try { po.setLatitude(new BigDecimal(reqJson.getString("latitude"))); } catch(Exception ex) {}
  377 + po.setPunchAddress(reqJson.containsKey("address") ? reqJson.getString("address") : "");
  378 + po.setStoreId("");
  379 + po.setWorkType("qrcode");
  380 + po.setPhotoUrl(reqJson.containsKey("photos") ? reqJson.getJSONArray("photos").getString(0) : "");
  381 + Object dao = ApplicationContextFactory.getBean("attendanceRecordV1ServiceDaoImpl");
  382 + if (dao != null) {
  383 + java.lang.reflect.Method m = dao.getClass().getMethod("saveAttendanceRecord", AttendanceRecordPo.class);
  384 + m.invoke(dao, po);
  385 + }
  386 + } catch (Exception e) {
  387 + logger.error("保存考勤记录失败", e);
  388 + }
  389 + }
  390 +
  391 + /**
  392 + * 二维码打卡距离校验(仅H5页面,不影响Android App)
  393 + */
  394 + private void checkQrcodeDistance(JSONObject reqJson) {
  395 + double userLng = Double.parseDouble(reqJson.getString("longitude"));
  396 + double userLat = Double.parseDouble(reqJson.getString("latitude"));
  397 +
  398 + // 查询巡检点坐标
  399 + InspectionDto pointDto = new InspectionDto();
  400 + pointDto.setInspectionId(reqJson.getString("inspectionId"));
  401 + pointDto.setCommunityId(reqJson.getString("communityId"));
  402 + List<InspectionDto> points =
  403 + inspectionPointV1InnerServiceSMOImpl.queryInspectionPoints(pointDto);
  404 + if (points == null || points.size() == 0) {
  405 + throw new CmdException("巡检点不存在");
  406 + }
  407 + String lng = points.get(0).getLng();
  408 + String lat = points.get(0).getLat();
  409 + if (StringUtil.isEmpty(lng) || StringUtil.isEmpty(lat)) {
  410 + return; // 无坐标时跳过距离校验
  411 + }
  412 + double pointLng = Double.parseDouble(lng);
  413 + double pointLat = Double.parseDouble(lat);
  414 +
  415 + // Haversine距离计算
  416 + double R = 6371;
  417 + double dLat = Math.toRadians(pointLat - userLat);
  418 + double dLng = Math.toRadians(pointLng - userLng);
  419 + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
  420 + + Math.cos(Math.toRadians(userLat)) * Math.cos(Math.toRadians(pointLat))
  421 + * Math.sin(dLng / 2) * Math.sin(dLng / 2);
  422 + double distance = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  423 +
  424 + if (distance > 2) {
  425 + throw new CmdException("距离巡检点超过2km,无法打卡(距离:" + String.format("%.2f", distance) + "km)");
  426 + }
  427 + }
329 428 }
330 429 \ No newline at end of file
... ...