瀏覽代碼

丰凯利考勤调整完成, 岩谷名片识别

pruple_boy 1 年之前
父節點
當前提交
2675fd505f
共有 61 個文件被更改,包括 18371 次插入28000 次删除
  1. 26 0
      mjava-fengkaili/pom.xml
  2. 117 69
      mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java
  3. 7 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/dao/FKLDdContactDao.java
  4. 19 1
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/entity/FKLDdContactPo.java
  5. 8 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/FKLService.java
  6. 491 19
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java
  7. 1 1
      mjava-fengkaili/src/main/resources/application-dev.yml
  8. 4 4
      mjava-fengkaili/src/main/resources/application-prod.yml
  9. 二進制
      mjava-fengkaili/src/main/resources/templates/Template_days.xlsx
  10. 二進制
      mjava-fengkaili/src/main/resources/templates/Template_month.xlsx
  11. 1 1
      mjava-fengkaili/target/classes/application-dev.yml
  12. 4 4
      mjava-fengkaili/target/classes/application-prod.yml
  13. 30 0
      mjava-gewu/src/main/java/com/malk/gewu/schedule/DDScheduleTask.java
  14. 0 2
      mjava-guyuan/pom.xml
  15. 0 1
      mjava-guyuan/src/main/java/com/malk/guyuan/controller/GYController.java
  16. 9 9
      mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java
  17. 0 75
      mjava-guyuan/src/main/java/com/malk/guyuan/server/model/sample.md
  18. 9 3
      mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/TXYInvoice.java
  19. 33 4
      mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java
  20. 8190 13811
      mjava-guyuan/src/main/resources/static/mjs/mjs.js
  21. 1 1
      mjava-guyuan/src/main/resources/static/mjs/mjs.min.js
  22. 8190 13811
      mjava-guyuan/target/classes/static/mjs/mjs.js
  23. 1 1
      mjava-guyuan/target/classes/static/mjs/mjs.min.js
  24. 3 33
      mjava-shangfeng/src/main/java/com/malk/shangfeng/controller/SFController.java
  25. 35 0
      mjava-shangfeng/src/main/java/com/malk/shangfeng/schedule/U8ScheduleTask.java
  26. 9 0
      mjava-shangfeng/src/main/java/com/malk/shangfeng/service/SFService.java
  27. 54 0
      mjava-shangfeng/src/main/java/com/malk/shangfeng/service/impl/SFImplService.java
  28. 1 1
      mjava-shangfeng/src/main/resources/application-dev.yml
  29. 1 1
      mjava-shangfeng/src/main/resources/application-prod.yml
  30. 64 0
      mjava-yangu/pom.xml
  31. 32 0
      mjava-yangu/src/main/java/com/malk/yangu/Boot.java
  32. 85 0
      mjava-yangu/src/main/java/com/malk/yangu/controller/YGController.java
  33. 26 0
      mjava-yangu/src/main/java/com/malk/yangu/filter/CatchException_YXY.java
  34. 190 0
      mjava-yangu/src/main/java/com/malk/yangu/server/model/McInvoiceDto.java
  35. 65 0
      mjava-yangu/src/main/java/com/malk/yangu/server/model/McInvoiceKind.java
  36. 34 0
      mjava-yangu/src/main/java/com/malk/yangu/server/tencent/TXYConf.java
  37. 37 0
      mjava-yangu/src/main/java/com/malk/yangu/tencent/TXYInvoice.java
  38. 156 0
      mjava-yangu/src/main/java/com/malk/yangu/tencent/impl/TXYImplInvoice.java
  39. 70 0
      mjava-yangu/src/main/resources/application-dev.yml
  40. 45 0
      mjava-yangu/src/main/resources/application-prod.yml
  41. 39 0
      mjava-yangu/src/test/resource/server.sh
  42. 2 2
      mjava-zhuogao/src/main/java/com/malk/zhuogao/controller/ZGController.java
  43. 1 1
      mjava/src/main/java/com/malk/base/BaseDao.java
  44. 1 1
      mjava/src/main/java/com/malk/base/BasePo.java
  45. 3 0
      mjava/src/main/java/com/malk/base/JpaMap.java
  46. 20 8
      mjava/src/main/java/com/malk/server/common/McPage.java
  47. 5 0
      mjava/src/main/java/com/malk/server/dingtalk/DDR_New.java
  48. 16 1
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_Attendance.java
  49. 15 1
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_Contacts.java
  50. 10 0
      mjava/src/main/java/com/malk/service/dingtalk/DDService.java
  51. 35 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Attendance.java
  52. 34 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Contacts.java
  53. 28 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplService.java
  54. 10 0
      mjava/src/main/java/com/malk/utils/UtilDateTime.java
  55. 23 5
      mjava/src/main/java/com/malk/utils/UtilExcel.java
  56. 1 1
      mjava/src/main/java/com/malk/utils/UtilFile.java
  57. 47 4
      mjava/src/main/java/com/malk/utils/UtilList.java
  58. 19 1
      mjava/src/main/java/com/malk/utils/UtilMap.java
  59. 13 3
      mjava/src/main/java/com/malk/utils/UtilNumber.java
  60. 0 120
      mjava/target/classes/META-INF/spring-configuration-metadata.json
  61. 1 0
      pom.xml

+ 26 - 0
mjava-fengkaili/pom.xml

@@ -25,4 +25,30 @@
             <version>${mjava.version}</version>
         </dependency>
     </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                    <!-- 如果没有该配置,devtools不会生效: 打包时关闭 -->
+                    <fork>false</fork>
+                    <!-- 避免中文乱码 -->
+                    <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
+                </configuration>
+                <!-- 允许生成可运行jar -->
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
 </project>

+ 117 - 69
mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java

@@ -4,23 +4,32 @@ package com.malk.fengkaili.controller;
  * 错误抛出与拦截详见 CatchException
  */
 
-import com.alibaba.fastjson.JSON;
 import com.malk.fengkaili.repository.entity.FKLDdContactPo;
 import com.malk.fengkaili.service.FKLService;
+import com.malk.server.common.McException;
+import com.malk.server.common.McPage;
 import com.malk.server.common.McR;
+import com.malk.server.dingtalk.DDConf;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Attendance;
 import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.utils.UtilExcel;
+import com.malk.utils.UtilList;
 import com.malk.utils.UtilMap;
-import com.malk.utils.UtilNumber;
+import com.malk.utils.UtilMc;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.*;
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -40,82 +49,121 @@ public class FKLController {
     @Autowired
     private FKLService fklService;
 
-    @PostMapping("test1")
-    McR test1() {
+    @Autowired
+    private DDConf ddConf;
+
+    /**
+     * 同步用户 & 部门
+     */
+    @PostMapping("syncUserInfo")
+    McR syncUserInfo() {
 
         fklService.syncUserInfo();
+        return McR.success();
+    }
+
+
+    /// 考勤汇总
+    private McPage _getAttendanceList(Map data, List<String> days) {
+        log.info("考勤汇总, {}", data);
+        McException.assertParamException_Null(data, "startTime", "endTime");
 
-        return McR.success(Arrays.asList(1, 3, 2, 4).stream().reduce(Integer::sum).orElse(0));
-//        return McR.success(ddClient_attendance.isOpenSmartReport(ddClient.getAccessToken()));
+        List<Long> dpetIds = (List<Long>) data.get("deptId");
+        // 基于用户分页
+        Page page = fklService.queryUserInfos(UtilMap.getInt(data, "page"), UtilMap.getInt(data, "size"), UtilMap.getString(data, "name"), dpetIds);
+        McException.assertAccessException(page.getTotalElements() == 0, "查询用户为空!");
+
+        List<FKLDdContactPo> userInfos = page.getContent();
+        List<Map> dataList = fklService.queryAttendanceList(data.get("startTime").toString(), data.get("endTime").toString(), userInfos, days);
+        log.info("汇总数量, {}", dataList.size());
+        return McPage.page(page, dataList);
     }
 
-    /// 累计月度汇总数字
-    private Object _reduceAttendance(Map column, String name, String keyList) {
-        Object value;
-        List<Map> vals = (List<Map>) column.get(keyList);
-        // 异常信息, 保留备注
-        if (name.equals("考勤结果")) {
-            value = String.join("\n", vals.stream().filter(item -> {
-                String content = UtilMap.getString(item, "value").replaceAll("正常", "").replaceAll("休息", "");
-                return StringUtils.isNotBlank(content);
-            }).map(item -> UtilMap.getString(item, "value")).collect(Collectors.toList()));
-        } else {
-            value = vals.stream().map(item -> UtilMap.getFloat(item, "value")).reduce((a, b) -> a + b).orElse(0f);
-        }
-        return value;
+    /**
+     * 查询考勤汇总
+     */
+    @PostMapping("queryAttendanceList")
+    McR queryAttendanceList(@RequestBody Map data) {
+
+        return McR.success(_getAttendanceList(data, null));
     }
 
-    @PostMapping("test")
-    McR test() {
+    /**
+     * 查询考勤汇总 [天]
+     */
+    @PostMapping("queryAttendanceDays")
+    McR queryAttendanceDays(@RequestBody Map data) {
+        List<String> days = new ArrayList<>();
 
-        // 考勤列, 假期信息定义
-        List<String> columnNames = Arrays.asList("旷工天数", "出勤天数", "工作时长", "考勤结果", "出差时长", "迟到次数", "早退次数", "下班缺卡次数", "上班缺卡次数", "外出时长", "休息日加班", "工作日加班", "节假日加班", "严重迟到次数", "应出勤天数");
-        List<Map> columns = ddClient_attendance.getAttColumns(ddClient.getAccessToken());
-        Map columnIds = new HashMap();
-        // 假期单独返回, 钉钉产品规则
-        List<String> leaveNames = columns.stream().filter(column -> {
-                    // 列类型储存id映射名称为map, 考勤数据返回仅保留列id
-                    if (columnNames.contains(column.get("name"))) {
-                        columnIds.put(column.get("id").toString(), column.get("name"));
-                        return false;
-                    }
-                    return column.get("alias").equals("leave_");
-                }
-        ).map(column -> String.valueOf(column.get("name"))).collect(Collectors.toList());
-
-
-        String start = "2023-07-01 00:00:00";
-        String end = "2023-07-22 23:59:59";
-        // 基于用户分页
-        List<FKLDdContactPo> userInfos = fklService.queryUserInfos(1, 200, null, null).getContent();
-
-        List<Map> attendanceInfos = new ArrayList<>();
-        List<String> queryIds = new ArrayList<>(columnIds.keySet()); // 考勤列定义
-        userInfos.forEach(po -> {
-            Map attendanceInfo = UtilMap.map("员工ID, 员工姓名, 员工工号, 所属部门", po.getUserId(), po.getName(), po.getJobNumber(), po.getDeptName());
-            // 累计月度汇总
-            ddClient_attendance.getAttColumnVal(ddClient.getAccessToken(), po.getUserId(), queryIds, start, end).forEach(column -> {
-                String id = ((Map) column.get("column_vo")).get("id").toString();
-                String name = String.valueOf(columnIds.get(id)); // 接口仅返回列id, 通过map映射
-                attendanceInfo.put(name, _reduceAttendance(column, name, "column_vals"));
-            });
-            // 累计假期数据
-            for (Map column : ddClient_attendance.getLeaveTimeByNames(ddClient.getAccessToken(), po.getUserId(), leaveNames, start, end)) {
-                String name = ((Map) column.get("columnvo")).get("name").toString(); // 接口返回列名称
-                attendanceInfo.put(name, _reduceAttendance(column, name, "columnvals"));
+        return McR.success(UtilMap.map("page, prop", _getAttendanceList(data, days), days));
+    }
+
+    /**
+     * 导出考勤汇总
+     */
+    @PostMapping("exportAttendanceList")
+    void exportAttendanceList(@RequestBody Map data, HttpServletResponse response) {
+
+        data.put("page", 1);
+        data.put("size", Integer.MAX_VALUE);
+        List<Map> dataList = _getAttendanceList(data, null).getList();
+        //  获取出现最多次作为法定应出勤天数, 考勤应出勤天数和班组 + 人员挂钩 [班次详情应出勤不准确]
+        float workdays = (Float) UtilList.maxFrequencyObject(dataList.stream().map(item -> {
+            float val = 0.f;
+            // 数据内0字段被忽略, 兼容处理
+            if (item.containsKey("出勤天数")) {
+                val = UtilMap.getFloat(item, "出勤天数");
             }
-            // 数据处理
-            attendanceInfo.put("加班总时长", UtilNumber.formatPrecision(UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班")));
-            attendanceInfos.add(attendanceInfo);
-        });
-        log.info("xxx, {}", JSON.toJSONString(attendanceInfos));
+            return val;
+        }).collect(Collectors.toList()));
+        String range = ("核算周期: " + data.get("startTime").toString().split(" ")[0].replace("-", ".") + "-" + data.get("endTime").toString().split(" ")[0].replace("-", "."));
+        String attendance = workdays + "天*8小时*60分钟=";
+        Map dataMain = UtilMap.map("核算周期, 应出勤天数, 应出勤分钟", range, attendance, workdays * 8f * 60f);
+
+        UtilExcel.exportMapAndListByTemplate(response, dataMain, dataList, Map.class, "月度汇总", "Template_month.xlsx");
+    }
 
-//        log.info("xxx, {}", JSON.toJSONString(userInfo));
+    /**
+     * 导出考勤汇总 [天]
+     */
+    @PostMapping("exportAttendanceDays")
+    void exportAttendanceDays(@RequestBody Map data, HttpServletResponse response) {
+
+        data.put("page", 1);
+        data.put("size", Integer.MAX_VALUE);
+
+//        List<String> days = new ArrayList<>();
+//        List<Map> dataList = _getAttendanceList(data, days).getList();
+//        List<String> columnHeader = UtilList.asList("序号", "姓名", "工号", "部门", "出勤天数", "年假(天)", "事假(小时)", "调休(小时)", "产假(天)", "陪产假(天)", "婚假(天)", "丧假(天)", "哺乳假(小时)", "病假(天)", "育儿假(天)");
+//        List<String> columnProp = UtilList.asList("序号", "员工姓名", "员工工号", "所属部门", "出勤天数", "年假", "事假", "调休", "产假", "陪产假", "婚假", "丧假", "哺乳假", "病假", "育儿假");
+//        // 动态列头名称
+//        columnHeader.addAll(4, days);
+//        columnProp.addAll(4, days);
+//
+//        UtilExcel.builder().heardList(columnHeader.toArray(new String[0])).heardKey(columnProp.toArray(new String[0]))
+//                .data(dataList).fileName("月度明细").build()
+//                .exportExcelByPoi(response);
+
+        // 动态表头模板导出
+        List<String> days = new ArrayList<>();
+        List<Map> dataList = _getAttendanceList(data, days).getList();
+        Map dataMain = new HashMap();
+        days.forEach(UtilMc.consumerWithIndex((item, index) -> {
+            dataMain.put("day" + (index + 1), item);
+        }));
+        UtilExcel.exportMapAndListByTemplate(response, dataMain, dataList, Map.class, "月度明细", "Template_days.xlsx");
 
+    }
+
+    @PostMapping("test")
+    McR test() {
 
-//        getLeaveTimeByNames
-//        ddClient_attendance.getAttColumns(ddClient.getAccessToken());
-//        getAttColumnVal
-        return McR.success(columns);
+//        return McR.success(ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), 711600043L));
+//        return McR.success(fklService.queryUserInfos(1, Integer.MAX_VALUE, null, null));
+//        return McR.success(ddClient_attendance.getAttendanceShiftDetail(ddClient.getAccessToken(), ddConf.getOperator(), "237885247"));
+//        return McR.success(ddClient_attendance.getAttendanceGroupSearch(ddClient.getAccessToken(), ddConf.getOperator(), "自动考勤"));
+//        return McR.success(ddClient_attendance.getAttendanceGroupDetail(ddClient.getAccessToken(), ddConf.getOperator(), "343575114"));
+//        return McR.success(ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), UtilDateTime.parseDate("2023-07-01"), null));
+        return McR.success(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "0953580166811961653"));
     }
 }

+ 7 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/dao/FKLDdContactDao.java

@@ -3,8 +3,11 @@ package com.malk.fengkaili.repository.dao;
 import com.malk.fengkaili.repository.entity.FKLDdContactPo;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
 
 import javax.transaction.Transactional;
+import java.util.Date;
 
 /**
  * 钉钉花名册同步
@@ -13,4 +16,8 @@ import javax.transaction.Transactional;
 public interface FKLDdContactDao extends JpaRepository<FKLDdContactPo, Long>, JpaSpecificationExecutor<FKLDdContactPo> {
 
     boolean existsByUserId(String userId);
+
+    @Modifying
+    @Query("update FKLDdContactPo set leaveDate = ?2 where userId = ?1")
+    void updateLeaveDate(String userId, Date leaveDate);
 }

+ 19 - 1
mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/entity/FKLDdContactPo.java

@@ -1,5 +1,6 @@
 package com.malk.fengkaili.repository.entity;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.malk.base.BasePo;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -8,6 +9,9 @@ import lombok.NoArgsConstructor;
 
 import javax.persistence.Entity;
 import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import java.util.Date;
 
 @Entity
 @Data
@@ -48,7 +52,21 @@ public class FKLDdContactPo extends BasePo {
     private String deptName;
 
     /**
-     * 同步备注
+     * 入职日期
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date hiredDate;
+
+    /**
+     * 离职日期
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date leaveDate;
+
+    /**
+     * 考勤备注 [无需打卡]
      */
     private String remark;
 }

+ 8 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/service/FKLService.java

@@ -4,6 +4,7 @@ import com.malk.fengkaili.repository.entity.FKLDdContactPo;
 import org.springframework.data.domain.Page;
 
 import java.util.List;
+import java.util.Map;
 
 public interface FKLService {
 
@@ -16,4 +17,11 @@ public interface FKLService {
      * 查询用户列表
      */
     Page<FKLDdContactPo> queryUserInfos(int page, int size, String name, List<Long> deptIds);
+
+    /**
+     * 考勤数据统计
+     *
+     * @param days 考勤明细日期表头
+     */
+    List<Map> queryAttendanceList(String start, String end, List<FKLDdContactPo> userInfos, List<String> days);
 }

+ 491 - 19
mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java

@@ -1,13 +1,19 @@
 package com.malk.fengkaili.service.impl;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.malk.fengkaili.repository.dao.FKLDdContactDao;
 import com.malk.fengkaili.repository.entity.FKLDdContactPo;
 import com.malk.fengkaili.service.FKLService;
 import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Attendance;
 import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDService;
+import com.malk.utils.UtilDateTime;
 import com.malk.utils.UtilList;
 import com.malk.utils.UtilMap;
+import com.malk.utils.UtilNumber;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
@@ -18,15 +24,17 @@ import org.springframework.data.jpa.domain.Specification;
 import org.springframework.stereotype.Service;
 
 import javax.persistence.criteria.Predicate;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 @Service
 @Slf4j
 public class FKLImplService implements FKLService {
 
-
     @Autowired
     private DDClient ddClient;
 
@@ -36,6 +44,12 @@ public class FKLImplService implements FKLService {
     @Autowired
     private FKLDdContactDao fklDdContactDao;
 
+    @Autowired
+    private DDClient_Attendance ddClient_attendance;
+
+    @Autowired
+    private DDService ddService;
+
     /**
      * 同步用户信息
      */
@@ -43,23 +57,39 @@ public class FKLImplService implements FKLService {
     public void syncUserInfo() {
         // 匹配部门信息, 全量
         ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
-            String deptName = ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), deptId).get("name").toString();
-            for (String userId : ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId)) {
-                if (fklDdContactDao.existsByUserId(userId)) {
-                    continue;
+            // String deptName = ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), deptId).get("name").toString();
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
+            if (userIds.size() > 0) {
+                // 获取部门层级拼接
+                String deptName = ddService.getUserDepartmentHierarchyJoin(ddClient.getAccessToken(), userIds.get(0), "-");
+                for (String userId : userIds) {
+                    // 牧语
+                    if ("0953580166811961653".equals(userId) || fklDdContactDao.existsByUserId(userId)) {
+                        continue;
+                    }
+                    Map userinfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
+                    // 员工信息表, 落库
+                    fklDdContactDao.save(FKLDdContactPo.builder()
+                            .userId(userId)
+                            .name(UtilMap.getString(userinfo, "name"))
+                            .jobNumber(UtilMap.getString(userinfo, "job_number"))
+                            .deptId(deptId)
+                            .deptName(deptName)
+                            .mobile(UtilMap.getString(userinfo, "mobile"))
+                            .hiredDate(userinfo.containsKey("hired_date") ? new Date(UtilMap.getLong(userinfo, "hired_date")) : null)
+                            .remark(UtilMap.getString(userinfo, "remark")) // 无需打卡 标记
+                            .build());
+                    log.info("同步#入职人员, {}", userinfo);
                 }
-                Map userinfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
-                // 员工信息表, 落库
-                fklDdContactDao.save(FKLDdContactPo.builder()
-                        .userId(userId)
-                        .name(UtilMap.getString(userinfo, "name"))
-                        .jobNumber(UtilMap.getString(userinfo, "job_number"))
-                        .deptId(deptId)
-                        .deptName(deptName)
-                        .mobile(UtilMap.getString(userinfo, "mobile"))
-                        .build());
             }
         });
+
+        // 同步离职人员, 标记离职日期
+        Date start = UtilDateTime.convertToDateFromLocalDateTime(UtilDateTime.firstDayOfLastMonth(LocalDateTime.now()));
+        ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), start, null).forEach(item -> {
+            log.info("同步#离职人员, {}", item);
+            fklDdContactDao.updateLeaveDate(item.get("userId"), UtilDateTime.parse(item.get("leaveTime"), "yyyy-MM-dd'T'HH:mm:ss"));
+        });
     }
 
     /**
@@ -69,7 +99,7 @@ public class FKLImplService implements FKLService {
     public Page<FKLDdContactPo> queryUserInfos(int page, int size, String name, List<Long> deptIds) {
 
         // 分页 & 排序
-        Sort sort = Sort.by(Sort.Direction.DESC, "deptId");
+        Sort sort = Sort.by(Sort.Direction.ASC, "deptName");
         Pageable pageable = PageRequest.of(page - 1, size, sort);
 
         // 查询条件: 姓名, 所属部门
@@ -86,4 +116,446 @@ public class FKLImplService implements FKLService {
         // 无数据时返回空列表
         return fklDdContactDao.findAll(specification, pageable);
     }
+
+    /// 累计月度汇总数字
+    private Object _reduceAttendance(Map column, String name, String keyList) {
+        Object value;
+        List<Map> vals = (List<Map>) column.get(keyList);
+        // 异常信息, 保留备注
+        if (name.equals("考勤结果")) {
+            List<String> tmps = new ArrayList<>(); // 同行出差会重复, 考勤结果要过滤
+            vals.stream().forEach(item -> {
+                // prd 异常补录当前日期
+                String content = UtilMap.getString(item, "value");
+                String svalue = content;
+                if (!content.contains("-")) {
+                    content += UtilMap.getString(item, "date").split(" ")[0];
+                }
+                content = content.replace("未打卡,", "").replace("正常,", "").replace("休息并打卡,", "").replace("休息,", "");
+                // 休息有外出/出差 , 正常带其他状态情况 || 超过90未打卡静默用户 || 被添加为协同人后, 钉钉也会记录一条出差
+                if (content.contains("出差")) {
+                    // 兼容出差中还有其他考勤结果, 以及还存在跨天的情况下
+                    List<String> arr = new ArrayList<>();
+                    for (String t : content.split(",")) {
+                        if (!arr.contains(t) && !tmps.stream().filter(s -> s.contains(t)).findAny().isPresent()) {
+                            arr.add(t);
+                        }
+                    }
+                    if (arr.size() == 0) {
+                        return;
+                    }
+                    content = String.join(",", arr);
+                }
+                boolean isFuture = UtilDateTime.parseLocalDateTime(UtilMap.getString(item, "date")).isAfter(LocalDateTime.now());
+                if (!isFuture && StringUtils.isNotBlank(svalue) && !tmps.contains(content) && !content.contains("休息") && !svalue.equals("正常") && !svalue.equals("未打卡")) {
+                    tmps.add(content);
+                }
+            });
+            value = String.join("; ", tmps);
+        } else {
+            value = vals.stream().map(item -> UtilMap.getFloat(item, "value")).reduce(0.f, (a, b) -> {
+                // ddExt: 出差默认是可重复提交, 且若被添加为协同人, 也会多累计一天出差 [但工作时长是正常]. 可开启不允许重复提交, 同样的同行人会冲突
+                if (name.equals("出差时长") && b > 1.0f) {
+                    b = 1.0f;
+                }
+                return a + b;
+            });
+        }
+        return value;
+    }
+
+
+    /// 缓存考勤自定义列
+    private List<Map> columns;
+
+    List<Map> getColumns() {
+        if (UtilList.isEmpty(columns)) {
+            columns = ddClient_attendance.getAttColumns(ddClient.getAccessToken());
+        }
+        return columns;
+    }
+
+    /**
+     * 考勤数据统计
+     */
+    @Override
+    public List<Map> queryAttendanceList(String start, String end, List<FKLDdContactPo> userInfos, List<String> days) {
+
+        // 考勤列, 假期信息定义
+        List<String> columnNames = Arrays.asList("旷工天数", "出勤天数", "工作时长", "考勤结果", "出差时长", "迟到次数", "早退次数", "下班缺卡次数", "上班缺卡次数", "外出时长", "休息日加班", "工作日加班", "节假日加班", "严重迟到次数", "应出勤天数");
+        List<Map> columns = getColumns();
+        Map columnIds = new HashMap();
+        // 假期单独返回, 钉钉产品规则
+        List<String> leaveNames = columns.stream().filter(column -> {
+                    // 列类型储存id映射名称为map, 考勤数据返回仅保留列id
+                    if (columnNames.contains(column.get("name"))) {
+                        columnIds.put(column.get("id").toString(), column.get("name"));
+                        return false;
+                    }
+                    return column.get("alias").equals("leave_");
+                }
+        ).map(column -> String.valueOf(column.get("name"))).collect(Collectors.toList());
+
+        // 考勤汇总数据
+        List<Map> attendanceInfos = new ArrayList<>();
+        List<String> queryIds = new ArrayList<>(columnIds.keySet()); // 考勤列定义
+        userInfos.forEach(po -> {
+            Map attendanceInfo = UtilMap.map("员工ID, 员工姓名, 员工工号, 所属部门, 考勤状态", po.getUserId(), po.getName(), po.getJobNumber(), po.getDeptName(), po.getRemark());
+            // 累计月度汇总
+            ddClient_attendance.getAttColumnVal(ddClient.getAccessToken(), po.getUserId(), queryIds, start, end).forEach(column -> {
+                String id = ((Map) column.get("column_vo")).get("id").toString();
+                String name = String.valueOf(columnIds.get(id)); // 接口仅返回列id, 通过map映射
+                attendanceInfo.put(name, _reduceAttendance(column, name, "column_vals"));
+                // prd [sheet2]每天考勤结果统计
+                if (!Objects.isNull(days) && name.equals("考勤结果")) {
+                    List<Map> vals = (List<Map>) column.get("column_vals");
+                    int index = 0;
+                    for (Map<String, String> val : vals) {
+                        index++;
+                        String date = val.get("date").replace(" 00:00:00", "").replace(LocalDate.now().getYear() + "-", "");
+                        String result = val.get("value").replace("休息并打卡,", "").replace("休息,", ""); // 休息有外出/出差;
+                        log.info("人员明细, {} - {}, {}", date, po.getName(), val.get("value"));
+                        String day_1 = "zc", day_2 = "zc", type = "zc"; // 异常类型
+                        if (result.contains("休息") || result.contains("加班") || (val.get("value").contains("休息,") && (!result.contains("出差") && !result.contains("婚假") && !result.contains("产假")))) {
+                            type = "公假"; // 包含休息, 休息加班打卡, 忽略跨休息日连续请假情况, prd 钉钉后台配置: 产假, 婚假按自然日
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (StringUtils.isBlank(result)) {
+                            type = "/"; // 新入职
+                            day_1 = "/";
+                            day_2 = "/";
+                        } else if (result.equals("正常") || (result.split(",").length == 2 && result.contains("外勤") && result.contains("补卡")) || result.equals("下班外勤") || result.equals("上班外勤") || result.equals("上班外勤,下班外勤")) {
+                            // 包含补卡, 一次外勤补卡, 外勤考勤情况 [调休会被标识为考勤正常]
+                            type = "zc";
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (result.contains("产假") || result.contains("陪产假") || result.contains("婚假") || result.contains("丧假")) {
+                            type = result.split("假")[0] + "假"; // 按天请假
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (result.contains("旷工") || result.equals("未打卡")) {
+                            type = "旷工"; // 兼容异常情况
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (result.contains("缺卡") && !result.contains("到")) {
+                            // prd 8点上班, 8点后请假或外出都是缺卡记录
+                            if (result.equals("上班缺卡")) {
+                                type = "缺卡";
+                                day_1 = type;
+                            }
+                            if (result.equals("下班缺卡")) {
+                                // prd 离职操作是直接删除, 会有一次打卡, 符合标记为zc
+                                if (ObjectUtil.isNotNull(po.getLeaveDate()) && date.equals(UtilDateTime.format(po.getLeaveDate(), "MM-dd"))) {
+                                    type = type.length() > 0 ? type : "zc";
+                                    day_2 = "zc";
+                                } else {
+                                    type = "缺卡";
+                                    day_2 = type;
+                                }
+                            }
+                        } else if (result.split(",").length <= 2 && (result.contains("迟到") || result.contains("早退"))) {
+                            // 兼容早退和迟到情况下, 还存在请假情况
+                            if (result.contains("迟到") && !result.contains("补卡申请")) {
+                                type = "迟到"; // 迟到状态标记
+                                float exception_duration = Float.valueOf((result.split(",")[0].split("分钟")[0].replace("上班迟到", "")));
+                                if (exception_duration >= 180f) {
+                                    // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                    day_1 = "迟到";
+                                }
+                            }
+                            if (result.contains("早退") && !result.contains("补卡申请")) {
+                                type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
+                                float exception_duration = Float.valueOf((result.split(",")[result.split(",").length - 1].split("分钟")[0].replace("下班早退", "")));
+                                if (exception_duration >= 180f) {
+                                    // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                    day_2 = "早退";
+                                }
+                            }
+                        } else {
+                            type = "";
+                            day_1 = "";
+                            day_2 = "";
+                            // 请假 & 出差
+                            for (String status : result.split(",")) {
+                                /// 过滤异常情况 &  未打卡判定为status, 非result
+                                if (status.contains("补卡申请") || status.contains("正常") || status.equals("未打卡")) {
+                                    continue;
+                                }
+                                if (status.contains("缺卡") || status.equals("未打卡") || status.contains("迟到") || status.contains("早退")) {
+                                    if (status.equals("上班缺卡")) {
+                                        type = "缺卡";
+                                        day_1 = "缺卡";
+                                    }
+                                    if (status.equals("下班缺卡")) {
+                                        type += "缺卡";
+                                        day_2 = "缺卡";
+                                    }
+                                    // 兼容早退和迟到情况下, 还存在请假情况
+                                    if (status.contains("迟到") || status.contains("早退")) {
+                                        if (status.contains("迟到")) {
+                                            type = "迟到"; // 迟到状态标记
+                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("上班迟到", "")));
+                                            if (exception_duration >= 180f) {
+                                                // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                                day_1 = "迟到";
+                                            }
+                                        }
+                                        if (status.contains("早退")) {
+                                            type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
+                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("下班早退", "")));
+                                            if (exception_duration >= 180f) {
+                                                // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                                day_2 = "早提";
+                                            }
+                                        }
+                                    }
+                                } else {
+                                    /// 请假数据处理 [小时情况]
+                                    String tmp = status.contains("调休") ? "调休" : status.split("假")[0] + "假"; // 异常类型
+                                    if (status.contains("外出") || Arrays.asList("调休", "哺乳假", "事假").contains(tmp)) {
+                                        if (result.contains("外出")) {
+                                            tmp = "外出";
+                                        }
+                                        // 外出, 调休, 事假, 哺乳假: 兼容9点申请, 排班是8点情况, 不记录缺卡
+                                        if (day_1.equals("缺卡") && result.contains("09:00")) {
+                                            day_1 = "";
+                                        }
+                                        // prd 请假3小时以内标记为zc, 按照小时请假 [调休, 哺乳假, 事假]
+                                        String[] arr = status.split(" ");
+                                        float hour = Float.valueOf((arr[arr.length - 1].replace("小时", "")));
+                                        if (hour < 3.0f) { // <3 同时9点申请标识zc, 避免不能统计外出情况
+                                            continue;
+                                        } else {
+                                            // prd 请假3小时以内标记为zc, 区分上午与下午, 午休从12-13分割
+                                            String sStart = status.split(" ")[1].split("到")[0].replace(":", "");
+                                            type = type.length() > 0 && !tmp.equals(type) ? type + " " + tmp : tmp; // 兼容一天提交两次外出情况
+                                            if (Integer.valueOf(sStart) >= 1200) {
+                                                day_2 = tmp;
+                                            } else {
+                                                String sEnd = status.split(" ")[2].replace(":", "");
+                                                if (Integer.valueOf(sStart) < 800) {
+                                                    sStart = "0800";
+                                                }
+                                                float hourZao = Duration.between(UtilDateTime.parseLocal(sStart, "HHmm"), UtilDateTime.parseLocal("1200", "HHmm")).toMinutes() / 60f;
+                                                if (hourZao >= 3.0f) {
+                                                    day_1 += day_1.length() > 0 ? " " + tmp : tmp;
+                                                }
+                                                if (Integer.valueOf(sEnd) > 1700) {
+                                                    sEnd = "1700";
+                                                }
+                                                float hourWan = Duration.between(UtilDateTime.parseLocal("1300", "HHmm"), UtilDateTime.parseLocal(sEnd, "HHmm")).toMinutes() / 60f;
+                                                if (hourWan > 3.0f) {
+                                                    day_2 += day_2.length() > 0 ? " " + tmp : tmp;
+                                                }
+                                                // 外出兼容9点申请, 排班是8点情况
+                                                if (result.contains("外出") && Integer.valueOf(sStart) <= 900 && Integer.valueOf(sEnd) >= 1700) {
+                                                    type = "外出";
+                                                    day_1 = type;
+                                                    day_2 = type;
+                                                }
+                                            }
+                                        }
+                                    } else if (status.contains("出差")) {
+                                        // 出差兼容, 半天, 外出, 请假等情况
+                                        type += type.length() > 0 ? (type.contains("出差") ? "" : " 出差") : "出差";
+                                        // 半天出差场景以及被添加为协同人后, 钉钉也会记录一条出差; 均循环进行处理, 即时出差覆盖即当天多次出差也可兼容
+                                        int sStart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
+                                        int sEnd = Integer.valueOf(status.split(" ")[2].replace(":", ""));
+                                        if (val.get("value").contains("休息")) {
+                                            day_1 = day_1.equals("") ? "公假" : day_1;
+                                            day_2 = day_2.equals("") ? "公假" : day_2;
+                                        }
+                                        if (sStart >= 1200 && date.equals(status.split(" ")[0].replace("出差", ""))) {
+                                            // 跨天: 日期相等, 且下午时间
+                                            day_2 = "出差";
+                                        } else if (sEnd <= 1300 && date.equals(status.split(" ")[1].split("到")[1])) {
+                                            // 跨天: 日期相等, 且上午时间
+                                            day_1 = "出差";
+                                        } else {
+                                            day_1 = "出差";
+                                            day_2 = "出差";
+                                        }
+                                    } else {
+                                        /// 请假数据处理 [半天情况]
+                                        String[] arr = status.split(" ");
+                                        int sstart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
+                                        float day = Float.valueOf((arr[arr.length - 1].replace("天", "")));
+                                        type = tmp;
+                                        if (day == 0.5) {
+                                            if (sstart >= 1200) {
+                                                day_2 = type;
+                                            } else {
+                                                day_1 = type;
+                                            }
+                                        } else {
+                                            day_1 = type;
+                                            day_2 = type;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        // 日期动态列头
+                        if (!days.contains(date)) {
+                            days.add(date);
+                        }
+                        attendanceInfo.put(date, type.length() == 0 ? "zc" : type);
+                        attendanceInfo.put("day" + index + "_1", day_1.length() == 0 ? "zc" : day_1);
+                        attendanceInfo.put("day" + index + "_2", day_2.length() == 0 ? "zc" : day_2);
+                    }
+                }
+            });
+            // 累计假期数据
+            float leave_duration = 0f;
+            float leave_all = 0f;
+            for (Map column : ddClient_attendance.getLeaveTimeByNames(ddClient.getAccessToken(), po.getUserId(), leaveNames, start, end)) {
+                String name = ((Map) column.get("columnvo")).get("name").toString(); // 接口返回列名称
+                float value = (Float) _reduceAttendance(column, name, "columnvals");
+                // prd 法定假期[除病假、事件、调休、产假外]请假时长 [调休, 事假, 哺乳假为小时, 其余半天为最小单位]
+                if (!Arrays.asList("病假", "事假", "调休", "产假").contains(name)) {
+                    if (name.equals("哺乳假")) {
+                        leave_duration += value * 60f;
+                    } else {
+                        leave_duration += value * 60f * 8f;
+                    }
+                }
+                if (Arrays.asList("调休", "事假", "哺乳假").contains(name)) {
+                    leave_all += value / 8f;
+                } else {
+                    leave_all += value;
+                }
+                attendanceInfo.put(name, value);
+            }
+            // 数据处理, 请假折算天
+            float overTime = UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班");
+            attendanceInfo.put("加班总时长", UtilNumber.formatPrecisionValue(overTime));
+            attendanceInfo.put("事假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "事假") / 8f));
+            attendanceInfo.put("哺乳假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "哺乳假") / 8f));
+            attendanceInfo.put("调休天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "调休") / 8f));
+            // prd 标记人离职时间, 提示异常考勤
+            float exception_duration = 0f;
+            if (ObjectUtils.isNotEmpty(po.getHiredDate()) && UtilDateTime.beforeAndEqual(UtilDateTime.parseDateTime(start), po.getHiredDate()) && UtilDateTime.afterAndEqual(UtilDateTime.parseDateTime(end), po.getHiredDate())) {
+                Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.contains("迟到") && item.contains(UtilDateTime.formatDate(po.getHiredDate()))).findAny();
+                if (optional.isPresent()) {
+                    exception_duration = Float.valueOf((optional.get().toString().split("分钟")[0].replace("上班迟到", "")));
+                    attendanceInfo.put("迟到次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "迟到次数") - 1));
+                }
+                attendanceInfo.put("考勤结果", "入职日期" + UtilDateTime.formatDate(po.getHiredDate()) + "; " + attendanceInfo.get("考勤结果"));
+            }
+            attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "上班缺卡次数") + UtilMap.getFloat(attendanceInfo, "下班缺卡次数")));
+            if (ObjectUtils.isNotEmpty(po.getLeaveDate()) && UtilDateTime.parseDateTime(start).before(po.getLeaveDate()) && UtilDateTime.parseDateTime(end).after(po.getLeaveDate())) {
+                Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.equals("下班缺卡" + UtilDateTime.formatDate(po.getLeaveDate()))).findAny();
+                if (optional.isPresent()) {
+                    exception_duration = 480f;
+                    attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "缺卡次数") - 1));
+                }
+                attendanceInfo.put("考勤结果", "离职日期" + UtilDateTime.formatDate(po.getLeaveDate()) + "; " + attendanceInfo.get("考勤结果"));
+            }
+            attendanceInfo.put("缺卡调整时长", UtilNumber.formatPrecisionValue(exception_duration));
+            // prd 总时长 = 工作时长 + 法定假期[除病假、事件、调休、产假外]请假时长 + 调休时长 - 加班时长【出差、外出不考勤但需要计入总工时,以申请时长为准,但外出可能为不足一天情况, 当天还有打卡: 目前先取系统默认】
+            float system_duration = UtilMap.getFloat(attendanceInfo, "工作时长");
+            float tiaoxiu_duration = UtilMap.getFloat(attendanceInfo, "调休") * 60f;
+            attendanceInfo.put("调休时长", UtilNumber.formatPrecisionValue(tiaoxiu_duration));
+            attendanceInfo.put("法定假调整时长", UtilNumber.formatPrecisionValue(leave_duration));
+            attendanceInfo.put("总时长", UtilNumber.formatPrecisionValue(system_duration + leave_duration + tiaoxiu_duration + exception_duration - overTime));
+            // ppExt 钉钉接口休息如出差半天系统也返回出勤天数1, 存在异常; 休息日加班也会记录为出勤, 考勤字段调整无效
+            attendanceInfo.put("出勤天数_prd", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数") - leave_all));
+            attendanceInfos.add(attendanceInfo);
+        });
+        // prd 26-25周期非自然月逻辑 [获取出现最多次作为法定应出勤天数] 考勤应出勤天数和班组 + 人员挂钩, ppExt 排班天数钉钉查询没有接口
+        float workMin = (Float) UtilList.maxFrequencyObject(attendanceInfos.stream().map(item -> UtilMap.getFloat(item, "出勤天数")).collect(Collectors.toList())) * 60 * 8;
+
+        // prd 数据处理
+        int order = 0;
+        for (Map attendance : attendanceInfos) {
+            if (attendance.containsKey("总时长") && workMin > 0) {
+                attendance.put("勤勉度系数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendance, "总时长") / workMin));
+            }
+            order++;
+            attendance.put("序号", String.valueOf(order));
+            // prd 月度汇总表和月度明细表是否可实现部分无需打卡的员工
+            if ("无需打卡".equals(attendance.get("考勤状态"))) {
+                attendance.put("旷工天数", 0);
+                attendance.put("缺卡次数", 0);
+                attendance.put("上班缺卡次数", 0);
+                attendance.put("下班缺卡次数", 0);
+                attendance.put("迟到次数", 0);
+                attendance.put("早退次数", 0);
+                if (!Objects.isNull(days)) {
+                    for (Object key : attendance.keySet()) {
+                        String prop = String.valueOf(key);
+                        if (prop.contains("_") || prop.contains("-")) {
+                            String val = String.valueOf(attendance.get(prop)).replace("旷工", "").replace("缺卡", "").replace("迟到", "").replace("早退", "");
+                            // 忽略考勤异常 | 考勤静默用户
+                            if (StringUtils.isBlank(val) || val.equals("/")) {
+                                attendance.put(prop, "zc");
+                            } else {
+                                attendance.put(prop, val);
+                            }
+                        }
+                    }
+                } else {
+                    List<String> vals = new ArrayList<>();
+                    for (String cont : String.valueOf(attendance.get("考勤结果")).split("; ")) {
+                        if (!cont.contains("旷工") && !cont.contains("缺卡") && !cont.contains("迟到") && !cont.contains("早退")) {
+                            vals.add(cont);
+                        }
+                    }
+                    attendance.put("考勤结果", String.join("; ", vals));
+                }
+            }
+            // prd 异常与假期统计对应状态数据, 出勤天数就是实际到公司工作的天数[zc状态], 兼容3小时需求逻辑
+            AtomicReference<Float> days_rest = new AtomicReference<>(0f);
+            AtomicReference<Float> days_tiaoxiu = new AtomicReference<>(0f);
+            AtomicReference<Float> days_shijia = new AtomicReference<>(0f);
+            AtomicReference<Float> days_burujia = new AtomicReference<>(0f);
+            AtomicReference<Float> days_waichu = new AtomicReference<>(0f);
+            AtomicReference<Float> days_chidao = new AtomicReference<>(0f);
+            AtomicReference<Float> days_zaotui = new AtomicReference<>(0f);
+            AtomicReference<Float> days_kuangong = new AtomicReference<>(0f);
+            attendance.put("出勤天数_prd", attendance.keySet().stream().reduce(0f, (acc, cur) -> {
+                if (cur.toString().contains("_")) {
+                    // 外出标注为zc?
+                    if (attendance.get(cur).equals("外出")) {
+//                        attendance.put(cur, "zc");
+                        days_waichu.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    }
+                    // 累计汇总天数
+                    if (attendance.get(cur).equals("zc")) {
+                        return Float.valueOf(String.valueOf(acc)) + 0.5;
+                    } else if (attendance.get(cur).equals("公假")) {
+                        days_rest.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("调休")) {
+                        days_tiaoxiu.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("事假")) {
+                        days_shijia.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("哺乳假")) {
+                        days_burujia.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("迟到")) {
+                        days_chidao.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("早退")) {
+                        days_zaotui.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    } else if (attendance.get(cur).equals("旷工")) {
+                        days_kuangong.updateAndGet(v -> new Float((float) (v + 0.5)));
+                    }
+                }
+                return acc;
+            }));
+            attendance.put("公假天数_prd", days_rest.get());
+            attendance.put("调休天数_prd", days_tiaoxiu.get());
+            attendance.put("事假天数_prd", days_shijia.get());
+            attendance.put("哺乳假天数_prd", days_burujia.get());
+            attendance.put("外出天数_prd", days_waichu.get());
+            attendance.put("迟到次数_prd", days_chidao.get());
+            attendance.put("早退次数_prd", days_zaotui.get());
+            attendance.put("旷工天数_prd", days_kuangong.get());
+        }
+
+
+        // 记录月度明细日期, 进行排序 [接口返回已排序]
+//        if (UtilList.isNotEmpty(days)) {
+//            Collections.sort(days, Comparator.comparingLong(o -> Long.valueOf(o.replace("-", ""))));
+//        }
+        return UtilList.ignoreListMapZero(attendanceInfos);
+    }
 }

+ 1 - 1
mjava-fengkaili/src/main/resources/application-dev.yml

@@ -55,6 +55,6 @@ dingtalk:
   corpId: dingade22a8c4fd34b8535c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]
 
 

+ 4 - 4
mjava-fengkaili/src/main/resources/application-prod.yml

@@ -1,6 +1,6 @@
 # 环境配置
 server:
-  port: 9011
+  port: 9012
   servlet:
     context-path: /api/fengkaili
 
@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
@@ -30,4 +30,4 @@ dingtalk:
   corpId: dingade22a8c4fd34b8535c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]

二進制
mjava-fengkaili/src/main/resources/templates/Template_days.xlsx


二進制
mjava-fengkaili/src/main/resources/templates/Template_month.xlsx


+ 1 - 1
mjava-fengkaili/target/classes/application-dev.yml

@@ -55,6 +55,6 @@ dingtalk:
   corpId: dingade22a8c4fd34b8535c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]
 
 

+ 4 - 4
mjava-fengkaili/target/classes/application-prod.yml

@@ -1,6 +1,6 @@
 # 环境配置
 server:
-  port: 9011
+  port: 9012
   servlet:
     context-path: /api/fengkaili
 
@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
@@ -30,4 +30,4 @@ dingtalk:
   corpId: dingade22a8c4fd34b8535c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]

+ 30 - 0
mjava-gewu/src/main/java/com/malk/gewu/schedule/DDScheduleTask.java

@@ -0,0 +1,30 @@
+package com.malk.gewu.schedule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class DDScheduleTask {
+
+    /**
+     * 每天凌晨4点同步
+     */
+    @Scheduled(cron = "* * 4 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 0 - 2
mjava-guyuan/pom.xml

@@ -53,9 +53,7 @@
             <artifactId>spire.pdf.free</artifactId>
             <version>5.1.0</version>
         </dependency>
-
     </dependencies>
-
     <build>
         <plugins>
             <plugin>

+ 0 - 1
mjava-guyuan/src/main/java/com/malk/guyuan/controller/GYController.java

@@ -65,7 +65,6 @@ public class GYController {
         return McR.success(ydClient.convertTemporaryUrl(param.get("url")));
     }
 
-
     /**
      * 全局查询子表单
      */

+ 9 - 9
mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java

@@ -91,16 +91,17 @@ public class IVController {
     /// PDF压缩转base64
     @SneakyThrows
     private String pdfUrlConvertBase64(String pdfUrl) {
-
+        String fileName = "tmp_" + new Date().getTime() + ".pdf";
         // 下载文件
-        File file = UtilFile.mkdirIfNot("tmp.pdf", filePath.getPath().getTmp());
+        File file = UtilFile.mkdirIfNot(fileName, filePath.getPath().getTmp());
         UtilHttp.doDownload(pdfUrl, file);
 
         // PDF压缩
         PdfDocument doc = new PdfDocument(); // 创建PdfDocument类的对象
         doc.loadFromFile(file.getAbsolutePath()); // 加载PDF文档
         doc.getFileInfo().setIncrementalUpdate(false); // 禁用增量更新
-        doc.setCompressionLevel(PdfCompressionLevel.Best); // 将压缩级别设置为最佳
+        doc.setCompressionLevel(PdfCompressionLevel.Normal); // 将压缩级别设置为最佳
+
         // 遍历文档页面
         for (int i = 0; i < doc.getPages().getCount(); i++) {
             PdfPageBase page = doc.getPages().get(i);  // 获取指定页面
@@ -110,14 +111,13 @@ public class IVController {
                 for (int j = 0; j < images.length; j++) {
                     PdfImageInfo image = images[j];  // 获取指定图片
                     PdfBitmap bp = new PdfBitmap(image.getImage());
-                    bp.setQuality(25); // 设置压缩质量
+                    bp.setQuality(30); // 设置压缩质量
                     page.replaceImage(j, bp); // 将原始图像替换为压缩图像
                 }
-            // 将结果文档保存至另一个PDF文档中: 覆盖
-            doc.saveToFile(file.getAbsolutePath());
-            doc.close();
         }
-
+        // 将结果文档保存至另一个PDF文档中: 覆盖
+        doc.saveToFile(file.getAbsolutePath());
+        doc.close();
         // PDF转base64, 无需透出本地文件地址
         String base64 = UtilFile.fileToBase64(file.getAbsolutePath());
         // 删除临时PDF文件
@@ -256,7 +256,7 @@ public class IVController {
                 String serialTips = serial + "有疑问";
                 try {
                     // ppExt: 识别与验真后抬头对比
-                    Map rsp = txyInvoice.VatInvoiceVerifyNew(dto.getKindName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips);
+                    Map rsp = txyInvoice.doVatInvoiceVerifyNew(dto.getKindName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips);
                     Map invoice = (Map) rsp.get("Invoice");
                     McException.assertAccessException(!dto.getBuyerName().equals(guyuanNameRepalce(invoice.get("BuyerName").toString())), serialTips + ", 购买方名称不匹配!");
                     McException.assertAccessException(!dto.getBuyerTaxId().equals(invoice.get("BuyerTaxCode")), serialTips + ", 购买方税号不匹配!");

+ 0 - 75
mjava-guyuan/src/main/java/com/malk/guyuan/server/model/sample.md

@@ -1,75 +0,0 @@
-## 退回处理
-
-```
-//---------------------- private ----------------------//
-
-// 加载即调用方法请在此处进行调用
-export async function _mjsInit() {
-  await mjs.init(this, { vconsole: false })
-  // 页面环境:0提交(其它),1查看,2编辑(审批)
-  if (mjs.env) {
-    // 记录修改前ids
-    mjs.pre_ids = this.$('tableField_krmybpq6').getValue().reduce((acc, cur) => {
-      acc.push(...cur.associationFormField_liya90jt.map(item => item.instanceId))
-      return acc;
-    }, [])
-    console.log("当前明细实例Ids", mjs.pre_ids);
-
-   }
-  // 价税合计,计算
-  function round(number, precision) {
-    return Math.round(+number + 'e' + precision) / Math.pow(10, precision);
-  }
-  mjs._calcTotal = mjs.optimize.debounce(() => {
-    const details = this.$('tableField_krmybpq6').getValue();
-    const total = details.reduce((acc, cur) => {
-      const count = (cur.associationFormField_liya90jt || []).reduce((sum, ass) => {
-        sum += Number(ass.subTitle)
-        return sum;
-      }, 0)
-      cur.numberField_krn4ig4x = round(count, 2);
-      acc += cur.numberField_krn4ig4x;
-      return acc;
-    }, 0)
-    this.$('numberField_krn54uoe').setValue(total)
-  }, 400)
-
-  // 退回点击事件注册 & 响应
-  mjs.bus.DOM_CALLBACK_APPROVE = async (_this, title) => {
-    console.log(_this, title, title == "提交")
-    if (title == "提交") {
-      const cur_ids = this.$('tableField_krmybpq6').getValue().reduce((acc, cur) => {
-        acc.push(...cur.associationFormField_liya90jt.map(item => item.instanceId))
-        return acc;
-      }, [])
-      // 接口处理: 释放修改删除的全部数据,占用重新提交的全部数据
-      mjs.request.xhr.doPost("https://mc.cloudpure.cn/frp/guyuan/invoice-zy", null, {
-        pre_ids: mjs.pre_ids,
-        pre_update: {
-          selectField_liihyrt6: "未使用",
-        },
-        cur_ids: cur_ids,
-        cur_update: {
-          selectField_liihyrt6: "已使用",
-          radioField_liw7rb2q: "否" // 提交后, 更新是否退回标识为否
-        }
-      })
-    }
-  }
-  mjs.dom.registerFlowEvent(this);
-}
-
-//---------------------- event ----------------------//
-
-// 页面节点加载渲染完毕
-export function didMount() {
-  // 工具库: mjs & 初始化
-  this._mjsLoad();
-}
-
-//子表分类求和
-export function onChange({ value, extra }) { 
-  // 价税合计,计算
-  mjs._calcTotal()
-}
-```

+ 9 - 3
mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/TXYInvoice.java

@@ -16,9 +16,9 @@ public interface TXYInvoice {
     /**
      * 通用票据识别(高级版)[返回结构化数据]
      *
-     * @param imageUrl 下载图片经Base64编码后不超过 8M。图片下载时间不超过 3 秒
+     * @param image 下载图片经Base64编码后不超过 8M。图片下载时间不超过 3 秒
      */
-    Map doRecognizeGeneralInvoice(String imageUrl) throws TencentCloudSDKException;
+    Map doRecognizeGeneralInvoice(String image) throws TencentCloudSDKException;
 
     /**
      * 发票验真[新版]
@@ -27,5 +27,11 @@ public interface TXYInvoice {
      * @param checkCode           校验码后 6 位,增值税普通发票、增值税电子普通发票、增值税普通发票(卷式)、增值税电子普通发票(通行费)时必填;
      * @param excludingTax/amount 不含税金额,增值税专用发票、增值税电子专用发票、机动车销售统一发票、二手车销售统一发票、区块链发票时必填; 全电发票为价税合计(含税金额)
      */
-    Map VatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException;
+    Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException;
+
+    /**
+     * 名片识别
+     */
+    Map doBusinessCardOCR(String image) throws TencentCloudSDKException;
+
 }

+ 33 - 4
mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java

@@ -55,7 +55,7 @@ public class TXYImplInvoice implements TXYInvoice {
             req.setImageBase64(image);
         }
         req.setReturnMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
-        log.debug("请求参数, {}", JSON.toJSONString(req));
+        //log.debug("请求参数, {}", JSON.toJSONString(req));
         MixedInvoiceOCRResponse resp = client.MixedInvoiceOCR(req);
         String result = MixedInvoiceOCRResponse.toJsonString(resp);
         log.debug("请求响应, {}", result);
@@ -68,13 +68,17 @@ public class TXYImplInvoice implements TXYInvoice {
      * @apiNote https://cloud.tencent.com/document/api/866/90802
      */
     @Override
-    public Map doRecognizeGeneralInvoice(String imageUrl) throws TencentCloudSDKException {
+    public Map doRecognizeGeneralInvoice(String image) throws TencentCloudSDKException {
         Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
         ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
         OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
 
         RecognizeGeneralInvoiceRequest req = new RecognizeGeneralInvoiceRequest();
-        req.setImageUrl(imageUrl);
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
         req.setEnableMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
         req.setEnableCutImage(true); // 返回切割图片base64
         RecognizeGeneralInvoiceResponse resp = client.RecognizeGeneralInvoice(req);
@@ -89,7 +93,7 @@ public class TXYImplInvoice implements TXYInvoice {
      * @apiNote https://cloud.tencent.com/document/product/866/73674
      */
     @Override
-    public Map VatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException {
+    public Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException {
         Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
         ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
         OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
@@ -128,4 +132,29 @@ public class TXYImplInvoice implements TXYInvoice {
         log.debug("请求响应, {}", result);
         return rsp;
     }
+
+    /**
+     * 名片识别
+     *
+     * @apiNote https://console.cloud.tencent.com/api/explorer?Product=ocr&Version=2018-11-19&Action=BusinessCardOCR
+     */
+    @Override
+    public Map doBusinessCardOCR(String image) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        BusinessCardOCRRequest req = new BusinessCardOCRRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        req.setConfig(JSON.toJSONString(UtilMap.map("RetImageType", "PROPROCESS")));
+        //log.debug("请求参数, {}", JSON.toJSONString(req));
+        BusinessCardOCRResponse resp = client.BusinessCardOCR(req);
+        String result = BusinessCardOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
 }

File diff suppressed because it is too large
+ 8190 - 13811
mjava-guyuan/src/main/resources/static/mjs/mjs.js


File diff suppressed because it is too large
+ 1 - 1
mjava-guyuan/src/main/resources/static/mjs/mjs.min.js


File diff suppressed because it is too large
+ 8190 - 13811
mjava-guyuan/target/classes/static/mjs/mjs.js


File diff suppressed because it is too large
+ 1 - 1
mjava-guyuan/target/classes/static/mjs/mjs.min.js


+ 3 - 33
mjava-shangfeng/src/main/java/com/malk/shangfeng/controller/SFController.java

@@ -4,56 +4,26 @@ package com.malk.shangfeng.controller;
  * 错误抛出与拦截详见 CatchException
  */
 
-import com.alibaba.fastjson.JSON;
 import com.malk.server.common.McR;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.server.dingtalk.DDFormComponentDto;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Workflow;
-import com.malk.shangfeng.repository.dao.Fitemss97Dao;
-import com.malk.shangfeng.repository.entity.Fitemss97Po;
-import com.malk.utils.UtilMap;
+import com.malk.shangfeng.service.SFService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.List;
-import java.util.Map;
-
 @Slf4j
 @RestController
 @RequestMapping
 public class SFController {
 
     @Autowired
-    private DDClient ddClient;
-
-    @Autowired
-    private DDConf ddConf;
-
-    @Autowired
-    private DDClient_Workflow workflow;
-
-    @Autowired
-    private Fitemss97Dao dao;
+    private SFService sfService;
 
     @GetMapping("test")
     McR test() {
 
-        for (Fitemss97Po po : dao.queryProject()) {
-            Map data = (Map) JSON.parse(JSON.toJSONString(po));
-            // 组件数据格式化
-            Map ruleForm = UtilMap.map("citemcode, citemname", "项目编码, 项目名称");
-            // 推送钉钉审批单
-            List<Map> formValues = DDFormComponentDto.formatComponentValues(data, ruleForm, null);
-            Map extInfo = UtilMap.map("dept_id", DDConf.TOP_DEPARTMENT);
-            workflow.doProcessInstances(ddClient.getAccessToken(), ddConf.getOperator(), Fitemss97Po.PROCESS_CODE, formValues, extInfo);
-            dao.updateProject(po.getCitemcode());
-            log.info("同步项目信息, {}", po);
-        }
-
+        sfService.syncU8Project();
         return McR.success();
     }
 }

+ 35 - 0
mjava-shangfeng/src/main/java/com/malk/shangfeng/schedule/U8ScheduleTask.java

@@ -0,0 +1,35 @@
+package com.malk.shangfeng.schedule;
+
+import com.malk.shangfeng.service.SFService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class U8ScheduleTask {
+
+    @Autowired
+    private SFService sfService;
+
+    /**
+     * 每天凌晨4点同步
+     */
+    @Scheduled(cron = "0 0 2 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+            sfService.syncU8Project();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 9 - 0
mjava-shangfeng/src/main/java/com/malk/shangfeng/service/SFService.java

@@ -0,0 +1,9 @@
+package com.malk.shangfeng.service;
+
+public interface SFService {
+
+    /**
+     * 同步u8项目档案
+     */
+    void syncU8Project();
+}

+ 54 - 0
mjava-shangfeng/src/main/java/com/malk/shangfeng/service/impl/SFImplService.java

@@ -0,0 +1,54 @@
+package com.malk.shangfeng.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDFormComponentDto;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Workflow;
+import com.malk.shangfeng.repository.dao.Fitemss97Dao;
+import com.malk.shangfeng.repository.entity.Fitemss97Po;
+import com.malk.shangfeng.service.SFService;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class SFImplService implements SFService {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDConf ddConf;
+
+    @Autowired
+    private DDClient_Workflow workflow;
+
+    @Autowired
+    private Fitemss97Dao dao;
+
+    /**
+     * 同步u8项目档案
+     */
+    @Override
+    public void syncU8Project() {
+
+        for (Fitemss97Po po : dao.queryProject()) {
+            Map data = (Map) JSON.parse(JSON.toJSONString(po));
+            // 组件数据格式化
+            Map ruleForm = UtilMap.map("citemcode, citemname", "项目编码, 项目名称");
+            // 推送钉钉审批单
+            List<Map> formValues = DDFormComponentDto.formatComponentValues(data, ruleForm, null);
+            Map extInfo = UtilMap.map("dept_id", DDConf.TOP_DEPARTMENT);
+            workflow.doProcessInstances(ddClient.getAccessToken(), ddConf.getOperator(), Fitemss97Po.PROCESS_CODE, formValues, extInfo);
+            dao.updateProject(po.getCitemcode());
+            log.info("同步项目信息, {}", po);
+        }
+
+    }
+}

+ 1 - 1
mjava-shangfeng/src/main/resources/application-dev.yml

@@ -43,6 +43,6 @@ dingtalk:
   corpId: ding4c21a1403de71296a1320dcb25e91351
   aesKey:
   token:
-  operator: "095358016621619612"   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "095358016621619612"   # 牧语 OA管理员账号 [0开头需要转一下字符串]
 
 

+ 1 - 1
mjava-shangfeng/src/main/resources/application-prod.yml

@@ -31,4 +31,4 @@ dingtalk:
   corpId: ding4c21a1403de71296a1320dcb25e91351
   aesKey:
   token:
-  operator: "685960294423265853"   # OA管理员账号 [0开头需要转一下字符串]
+  operator: "170820203423316720"   # 孙杰奇 OA管理员账号 [0开头需要转一下字符串]

+ 64 - 0
mjava-yangu/pom.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>java-mcli</artifactId>
+        <groupId>com.malk</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>mjava-yangu</artifactId>
+    <description>岩谷腾讯云名片识别</description>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.malk</groupId>
+            <artifactId>mjava</artifactId>
+            <version>${mjava.version}</version>
+        </dependency>
+        <!-- 腾讯云 [go to https://search.maven.org/search?q=tencentcloud-sdk-java and get the latest version.] -->
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java</artifactId>
+        </dependency>
+        <!-- 图片压缩 -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>0.4.8</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                    <!-- 如果没有该配置,devtools不会生效: 打包时关闭 -->
+                    <fork>false</fork>
+                    <!-- 避免中文乱码 -->
+                    <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
+                </configuration>
+                <!-- 允许生成可运行jar -->
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+</project>

+ 32 - 0
mjava-yangu/src/main/java/com/malk/yangu/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.yangu;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+import javax.persistence.EntityManager;
+
+/**
+ * corp项目: 扫描公共模块
+ * -
+ * 若是无需数据库模块, 配置无效地址也可启动, 引入mjava不支持直接 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 配置
+ * 需要配置 jpa.hibernate.ddl-auto 为 none. 标识对表没有任何操作. 若不设置为 non, flyway.enabled 配置会无效, 在没有数库连接情况下程序无法启动
+ */
+@EnableJpaAuditing
+@SpringBootApplication(scanBasePackages = {"com.malk"})
+public class Boot {
+
+    public static void main(String... args) {
+        SpringApplication.run(Boot.class, args);
+    }
+
+    /**
+     * 让Spring管理JPAQueryFactory [不使用Qualifier详见mjava-Boot]
+     */
+    @Bean
+    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
+        return new JPAQueryFactory(entityManager);
+    }
+}

+ 85 - 0
mjava-yangu/src/main/java/com/malk/yangu/controller/YGController.java

@@ -0,0 +1,85 @@
+package com.malk.yangu.controller;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.service.aliwork.YDClient;
+import com.malk.utils.UtilMap;
+import com.malk.yangu.tencent.TXYInvoice;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import net.coobird.thumbnailator.Thumbnails;
+import org.apache.poi.util.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.*;
+
+@Slf4j
+@RestController
+@RequestMapping
+public class YGController {
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private TXYInvoice txyInvoice;
+
+    /// url压缩转base64
+    @SneakyThrows
+    private String imageUrlConvertBase64(String imageUrl) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        // scale(比例), outputQuality(质量)
+        Thumbnails.fromURLs(Arrays.asList(new URL(imageUrl))).scale(0.5f).outputQuality(0.25f).toOutputStream(out);
+        InputStream inputStream = new ByteArrayInputStream(out.toByteArray());
+        //转换为base64
+        byte[] bytes = IOUtils.toByteArray(inputStream);
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    /// 优先获取字段, 新版本接口已支持字段返回
+    private String findValue(List<Map> infos, String... names) {
+        for (String name : names) {
+            Optional optional = infos.stream().filter(info -> info.get("Name").equals(name)).findAny();
+            if (optional.isPresent()) {
+                return String.valueOf(((Map) optional.get()).get("Value"));
+            }
+        }
+        return "";
+    }
+
+    /**
+     * 名片识别
+     */
+    @PostMapping("card-mp")
+    McR card_mp(@RequestBody Map<String, ?> data) throws TencentCloudSDKException {
+        McException.assertParamException_Null(data, "url");
+        String image = ydClient.convertTemporaryUrl(data.get("url").toString());
+        log.info("名片识别, 免登地址, {}", image);
+        // 非PDF, 且内存大于3M, 压缩后上传
+        if (UtilMap.getFloat(data, "size") > 3.0f) {
+            image = imageUrlConvertBase64(image);
+        }
+        List<Map> results = (List<Map>) txyInvoice.doBusinessCardOCR(image).get("BusinessCardInfos");
+
+        // 数据字段拼接 [formData.put("textField_lki2ir0k", findValue(results, "手机", "电话"));]
+        Map<String, String> compIds = (Map) data.get("compIds");
+        Map formData = new HashMap();
+        for (String compId : compIds.keySet()) {
+            formData.put(compId, findValue(results, compIds.get(compId).split(", ")));
+        }
+        return McR.success(formData);
+    }
+}

+ 26 - 0
mjava-yangu/src/main/java/com/malk/yangu/filter/CatchException_YXY.java

@@ -0,0 +1,26 @@
+package com.malk.yangu.filter;
+
+import com.malk.Filter.CatchException;
+import com.malk.server.common.McR;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 腾讯云 [通用拦截参考 CatchException]
+ */
+@Slf4j
+@RestControllerAdvice(annotations = RestController.class)
+public class CatchException_YXY extends CatchException {
+
+    /**
+     * 错误类抛出
+     */
+    @ExceptionHandler(TencentCloudSDKException.class)
+    public McR TencentCloudSDKException(TencentCloudSDKException e) {
+        log.error(e.getMessage(), e);  // 记录错误日志
+        return McR.errorVendor(e.getMessage(), "tencent");
+    }
+}

+ 190 - 0
mjava-yangu/src/main/java/com/malk/yangu/server/model/McInvoiceDto.java

@@ -0,0 +1,190 @@
+package com.malk.yangu.server.model;
+
+import com.malk.base.BaseDto;
+import com.malk.server.common.McException;
+import com.malk.server.common.McREnum;
+import com.malk.utils.UtilMap;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class McInvoiceDto extends BaseDto {
+
+
+    /**************** 发票 ****************/
+
+    /**
+     * 发票名称
+     */
+    private String name;
+
+    // 兼容: 不为空
+    public String getName() {
+        if (StringUtils.isBlank(name)) {
+            return kindName;
+        }
+        return name;
+    }
+
+    /**
+     * 发票类型
+     */
+    private String kindName;
+
+    /**
+     * 发票类型 [编码]
+     */
+    private int kind;
+
+    /**
+     * 发票代码
+     */
+    private String code;
+
+    /**
+     * 发票号码
+     */
+    private String serial;
+
+    /**
+     * 开票日期 [yyyy-MM-dd]
+     */
+    private String date;
+
+    /**
+     * 校验码
+     */
+    private String checkCode;
+
+    /**
+     * 价税合计
+     */
+    private BigDecimal amount;
+
+    /**
+     * 不含税金额
+     */
+    private BigDecimal excludingTax;
+
+    /**
+     * 不含税金额
+     */
+    private BigDecimal tax;
+
+    /**
+     * 购买方名称
+     */
+    private String buyerName;
+
+    /**
+     * 购买方税号
+     */
+    private String buyerTaxId;
+
+    /**
+     * 销售方名称
+     */
+    private String sellerName;
+
+    /**
+     * 销售方税号
+     */
+    private String sellerTaxId;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**************** 交通 ****************/
+
+    /**
+     * 乘客姓名
+     */
+    private String passengerName;
+
+    /**
+     * 座位类型
+     */
+    private String seatType;
+
+    /**
+     * 出发地
+     */
+    private String departurePort;
+
+    /**
+     * 到达地
+     */
+    private String arrivePort;
+
+    /**
+     * 出发时间 [yyyy-MM-dd HH:mm]
+     */
+    private String departureTime;
+
+    /**
+     * 车次编号/航班号
+     */
+    private String trainNo;
+
+    /**
+     * 保险费
+     */
+    private BigDecimal insuranceCosts;
+
+    /**
+     * 燃油附加费
+     */
+    private BigDecimal fuelCosts;
+
+    /**
+     * 民航发展基金
+     */
+    private BigDecimal constructionCosts;
+
+    /**************** 格式化 ****************/
+
+    /**
+     * 实例Map, 服务宜搭组件映射 [明细组件获取表头, 通过名称转换字段]
+     *
+     * @implSpec const headers = this.$("tableField_liv5f4d2").props.children[0].props.children.map(({ props: { label, fieldId } }) => ({ label, compId: fieldId }))
+     */
+    public static Map formatDtoLabelAndProp() {
+
+        Map data = UtilMap.map("发票名称, 发票类型, 发票代码, 发票号码, 开票日期, 校验码, 价税合计, 不含税金额, 税额", "name, kindName, code, serial, date, checkCode, amount, excludingTax, tax");
+        data.putAll(UtilMap.map("购买方名称, 购买方税号, 销售方名称, 销售方税号", "buyerName, buyerTaxId, sellerName, sellerTaxId"));
+        data.putAll(UtilMap.map("乘客姓名, 座位类型, 出发地, 到达地, 出发时间, 车次编号/航班号, 保险费, 燃油附加费, 民航发展基金", "passengerName, seatType, departurePort, arrivePort, departureTime, trainNo, insuranceCosts, fuelCosts, constructionCosts"));
+        return data;
+    }
+
+    /**
+     * 格式化返回
+     */
+    public static Map formatResponse(Object result) {
+        return UtilMap.map("result, dto", result, formatDtoLabelAndProp());
+    }
+
+    /**************** 返回值 ****************/
+
+    // 成功状态标记
+    private final static String SUC_CODE = "OK";
+
+    /**
+     * 断言错误信息
+     */
+    public static void assertSuccess(Map result, String kind) {
+        String code = UtilMap.getString(result, "Code");
+        McException.assertException(!SUC_CODE.equals(code), McREnum.VENDOR_ERROR.getCode(), kind, "tencent");
+    }
+}
+

+ 65 - 0
mjava-yangu/src/main/java/com/malk/yangu/server/model/McInvoiceKind.java

@@ -0,0 +1,65 @@
+package com.malk.yangu.server.model;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * 定义返回值和对应状态的信息
+ */
+@Slf4j
+public enum McInvoiceKind {
+
+    PP("增值税普通发票", 1),
+    ZP("增值税专用发票", 2),
+    DP("增值税电子普通发票", 3),
+    DZ("增值税电子专用发票", 4),
+    QP("全电普通发票", 5),
+    QZ("全电专用发票", 6),
+
+    HC("火车票", 11),
+    JP("机票行程单", 12),
+    CZC("出租车发票", 13),
+    DE("定额发票", 14),
+    GLGQ("过路过桥费发票", 15),
+
+    FS("非税发票", 21),
+    OT("其它特殊发票", 22),
+
+    UN("未知类型", -1);
+
+    @Getter
+    private String desc;
+    @Getter
+    private int code;
+
+    /**
+     * 根据code查找
+     */
+    public final static String getKindName(int code) {
+        Optional optional = Arrays.stream(McInvoiceKind.values()).filter(item -> item.code == code).findAny();
+        if (optional.isPresent()) {
+            return ((McInvoiceKind) optional.get()).desc;
+        }
+        return UN.getDesc();
+    }
+
+    /**
+     * 根据name查找
+     */
+    public final static int getKindCode(String name) {
+        Optional optional = Arrays.stream(McInvoiceKind.values()).filter(item -> item.desc.equals(name)).findAny();
+        //log.info("发票类型, {}", Arrays.stream(McInvoiceKind.values()).map(item -> item.desc).collect(Collectors.toList()));
+        if (optional.isPresent()) {
+            return ((McInvoiceKind) optional.get()).code;
+        }
+        return UN.getCode();
+    }
+
+    McInvoiceKind(String name, int code) {
+        this.desc = name;
+        this.code = code;
+    }
+}

+ 34 - 0
mjava-yangu/src/main/java/com/malk/yangu/server/tencent/TXYConf.java

@@ -0,0 +1,34 @@
+package com.malk.yangu.server.tencent;
+
+import com.malk.utils.UtilMap;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 读取配置文件参考FilePah
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "tencent")
+public class TXYConf {
+
+    private String APPID;
+
+    private String SecretId;
+
+    private String SecretKey;
+
+    private String Region;
+
+    /**
+     * 票据类型
+     * -
+     * 中央非税未返回税号官方说明: 非税发票理论是没有税号的,图片中那个属于信用代码 [两个版本接口均为返回]
+     */
+    public static final Map<String, String> TYPE_INVOICE = UtilMap.map("-1, 0, 1, 2, 3, 5, 8, 9, 10, 11, 12, 13, 15, 16",
+            "未知类型, 出租车发票, 定额发票, 火车票, 增值税发票, 机票行程单, 通用机打发票, 汽车票, 轮船票, 增值税发票(卷票) , 购车发票, 过路过桥费发票, 非税发票, 全电发票");
+
+}

+ 37 - 0
mjava-yangu/src/main/java/com/malk/yangu/tencent/TXYInvoice.java

@@ -0,0 +1,37 @@
+package com.malk.yangu.tencent;
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+
+import java.util.Map;
+
+public interface TXYInvoice {
+
+    /**
+     * 混贴票据识别 [将弃用, 直接返回识别数据, 官方停止更新]
+     *
+     * @param image 下载图片经Base64编码后不超过 7M。图片下载时间不超过 3 秒
+     */
+    Map doMixedInvoiceOCR(String image) throws TencentCloudSDKException;
+
+    /**
+     * 通用票据识别(高级版)[返回结构化数据]
+     *
+     * @param imageUrl 下载图片经Base64编码后不超过 8M。图片下载时间不超过 3 秒
+     */
+    Map doRecognizeGeneralInvoice(String imageUrl) throws TencentCloudSDKException;
+
+    /**
+     * 发票验真[新版]
+     *
+     * @param invoiceCode         票代码(10或12 位),全电发票为空
+     * @param checkCode           校验码后 6 位,增值税普通发票、增值税电子普通发票、增值税普通发票(卷式)、增值税电子普通发票(通行费)时必填;
+     * @param excludingTax/amount 不含税金额,增值税专用发票、增值税电子专用发票、机动车销售统一发票、二手车销售统一发票、区块链发票时必填; 全电发票为价税合计(含税金额)
+     */
+    Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException;
+
+    /**
+     * 名片识别
+     */
+    Map doBusinessCardOCR(String image) throws TencentCloudSDKException;
+
+}

+ 156 - 0
mjava-yangu/src/main/java/com/malk/yangu/tencent/impl/TXYImplInvoice.java

@@ -0,0 +1,156 @@
+package com.malk.yangu.tencent.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.server.common.McException;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilNumber;
+import com.malk.yangu.server.tencent.TXYConf;
+import com.malk.yangu.tencent.TXYInvoice;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.ocr.v20181119.OcrClient;
+import com.tencentcloudapi.ocr.v20181119.models.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Slf4j
+@Service
+public class TXYImplInvoice implements TXYInvoice {
+
+    @Autowired
+    private TXYConf txyConf;
+
+    // 创建请求描述
+    private ClientProfile doRequest(String endPoint) {
+
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint(endPoint);
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        return clientProfile;
+    }
+
+    /**
+     * 混贴票据识别
+     *
+     * @apiNote https://cloud.tencent.com/document/product/866/37835
+     */
+    @Override
+    public Map doMixedInvoiceOCR(String image) throws TencentCloudSDKException {
+
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        MixedInvoiceOCRRequest req = new MixedInvoiceOCRRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        req.setReturnMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
+        log.debug("请求参数, {}", JSON.toJSONString(req));
+        MixedInvoiceOCRResponse resp = client.MixedInvoiceOCR(req);
+        String result = MixedInvoiceOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+
+    /**
+     * 通用票据识别(高级版)
+     *
+     * @apiNote https://cloud.tencent.com/document/api/866/90802
+     */
+    @Override
+    public Map doRecognizeGeneralInvoice(String imageUrl) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        RecognizeGeneralInvoiceRequest req = new RecognizeGeneralInvoiceRequest();
+        req.setImageUrl(imageUrl);
+        req.setEnableMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
+        req.setEnableCutImage(true); // 返回切割图片base64
+        RecognizeGeneralInvoiceResponse resp = client.RecognizeGeneralInvoice(req);
+        String result = MixedInvoiceOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+
+    /**
+     * 发票验真[新版]
+     *
+     * @apiNote https://cloud.tencent.com/document/product/866/73674
+     */
+    @Override
+    public Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        if (StringUtils.isNotBlank(checkCode)) {
+            checkCode = checkCode.substring(checkCode.length() - 6);
+        }
+
+        VatInvoiceVerifyNewRequest req = new VatInvoiceVerifyNewRequest();
+        req.setInvoiceNo(invoiceNo);
+        req.setInvoiceDate(invoiceDate);
+        req.setInvoiceCode(invoiceCode);
+        req.setCheckCode(checkCode);
+        if (invoiceKind.contains("全电")) {
+            // 全电票, 需要价税合计且无发票代码与校验码
+            req.setCheckCode(null);
+            req.setInvoiceCode(null);
+            req.setAmount(amount);
+        } else {
+            req.setAmount(null);
+            req.setAmount(excludingTax);
+        }
+        log.info("发票验真, {}", JSON.toJSONString(req));
+        VatInvoiceVerifyNewResponse resp = client.VatInvoiceVerifyNew(req);
+        String result = VatInvoiceVerifyNewResponse.toJsonString(resp);
+        Map rsp = (Map) JSON.parse(result);
+
+        // 因全电票取值, 取值价税合计, 单独校验下金额
+        Map invoice = (Map) rsp.get("Invoice");
+        if (StringUtils.isBlank(tips)) {
+            tips = "发票有疑问";
+        }
+        McException.assertAccessException(!UtilNumber.equalBigDecimal(UtilMap.getString(invoice, "AmountWithTax"), amount), tips + ", 价税合计金额不匹配!");
+        McException.assertAccessException(!UtilNumber.equalBigDecimal(UtilMap.getString(invoice, "AmountWithoutTax"), excludingTax), tips + ", 不含税金额不匹配!");
+
+        log.debug("请求响应, {}", result);
+        return rsp;
+    }
+
+    /**
+     * 名片识别
+     *
+     * @apiNote https://console.cloud.tencent.com/api/explorer?Product=ocr&Version=2018-11-19&Action=BusinessCardOCR
+     */
+    @Override
+    public Map doBusinessCardOCR(String image) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        BusinessCardOCRRequest req = new BusinessCardOCRRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        // req.setConfig(JSON.toJSONString(UtilMap.map("RetImageType", "PROPROCESS")));
+        log.debug("请求参数, {}", JSON.toJSONString(req));
+        BusinessCardOCRResponse resp = client.BusinessCardOCR(req);
+        String result = BusinessCardOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+}

+ 70 - 0
mjava-yangu/src/main/resources/application-dev.yml

@@ -0,0 +1,70 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/yangu
+
+# condition
+spel:
+  scheduling: false        # 定时任务是否执行
+  multiSource: false       # 是否多数据源配置
+
+spring:
+  # database
+  datasource:
+    hikari:
+      connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_ZL5NE83JE84UUACZDD03
+  systemToken: 3J966U61PPNCA6ROEIX8L8TY6DF33EYW60BKLY
+
+# tencent [腾讯云]
+tencent:
+  APPID: 1309939821
+  SecretId: AKID2uqoryukbO2XuBThuxzdEpnmnmoocuCH
+  SecretKey: wnmgYHo8wrmjlldKoHnIkDZlqvrVDpOz
+  Region: ap-shanghai

+ 45 - 0
mjava-yangu/src/main/resources/application-prod.yml

@@ -0,0 +1,45 @@
+# 环境配置
+server:
+  port: 9013
+  servlet:
+    context-path: /api/yangu
+
+# condition
+spel:
+  scheduling: true        # 定时任务是否执行
+  multiSource: false      # 是否多数据源配置
+
+spring:
+  # database
+  datasource:
+    hikari:
+      connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# dingtalk
+dingtalk:
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_ZL5NE83JE84UUACZDD03
+  systemToken: 3J966U61PPNCA6ROEIX8L8TY6DF33EYW60BKLY
+
+# tencent [腾讯云]
+tencent:
+  APPID: 1309939821
+  SecretId: AKID2uqoryukbO2XuBThuxzdEpnmnmoocuCH
+  SecretKey: wnmgYHo8wrmjlldKoHnIkDZlqvrVDpOz
+  Region: ap-shanghai

+ 39 - 0
mjava-yangu/src/test/resource/server.sh

@@ -0,0 +1,39 @@
+#!/bin/bash
+
+appname='mjava-yangu'
+
+if [ "$1" == "dev" ]; then
+  java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=dev
+else
+  if [ "$1" == "start" ]; then
+    nohup java -Xms256m -Xmx256m -jar $appname.jar &
+    echo "server prod is starting"
+    tail -f log/info.log
+  else
+    if [ "$1" == "test" ]; then
+      nohup java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=test &
+      echo "server test is starting"
+      tail -f log/info.log
+    else
+      if [ "$1" == "stop" ]; then
+        PID=$(ps -ef | grep $appname.jar | grep -v grep | awk '{ print $2 }')
+        if [ -z "$PID" ]; then
+          echo "server is already stopped"
+        else
+          echo kill $PID
+          kill $PID
+        fi
+      else
+        if [ "$1" == "status" ]; then
+          PID=$(ps -ef | grep $appname.jar | grep -v grep | awk '{ print $2 }')
+          if [ -z "$PID" ]; then
+            echo "server is stopped"
+          else
+            echo "server is running"
+            echo $PID
+          fi
+        fi
+      fi
+    fi
+  fi
+fi

+ 2 - 2
mjava-zhuogao/src/main/java/com/malk/zhuogao/controller/ZGController.java

@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
 @RestController
 @RequestMapping
 public class ZGController {
-    
+
     @Autowired
     private DDClient ddClient;
 
@@ -56,7 +56,7 @@ public class ZGController {
      * 导出审计操作记录
      */
     @PostMapping("records/export")
-    public void exprotRecords(HttpServletResponse response, @RequestBody Map param) {
+    public void exportRecords(HttpServletResponse response, @RequestBody Map param) {
 
         log.info("导出审计操作记录 入参, {}", param);
 

+ 1 - 1
mjava/src/main/java/com/malk/base/BaseDao.java

@@ -10,7 +10,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
  * @Table指定表【下划线和小驼峰都可】, 若使用驼峰则依然会有报错提示. 若类名从驼峰转为下划线和表名相同则可不需要注解
  * -
  * 自定义查询
- * @Query 当设置nativeQuery=true即可以使用原生SQL进行查询, 默认为false, 使用JPQL语法 [JPQL不支持insert语法]
+ * @Query 当设置nativeQuery=true即可以使用原生SQL进行查询, 默认为false, 使用JPQL语法 [JPQL不支持insert语法, 注意: nativeQuery查询使用表名, 不能使用对象名]
  * - 索引参数: 索引值从1开始,查询中"?X"个数需要与方法定义的参数个数相一致,并且顺序也要一致
  * - 命名参数: 可以定义好参数名,赋值时使用@Param("参数名"), 而不用管顺序. 对入参使用 @Param("email") 修饰, @Query内使用 :email 进行取值
  * - 关于返回值: update 语法, 返回值为 void. select 语法, 返回单个可用集合接收; 若返回多个用对象接收就会异常

+ 1 - 1
mjava/src/main/java/com/malk/base/BasePo.java

@@ -34,7 +34,7 @@ import java.util.Date;
 @EntityListeners(AuditingEntityListener.class)
 public abstract class BasePo extends BaseDto {
 
-    // 若是实体若不直接在 com.mcli 下, 可声明继承id, 避免编辑器提示 [不加也不影响编译以及运行]
+    // 若是实体若不直接在 com.malk 下, 可声明继承id, 避免编辑器提示 [不加也不影响编译以及运行] [ppExt: 现有表如u8, 不继承 BaseDto, 避免默认id与时间字段匹配异常]
     @ExcelIgnore
     @Id
     @JsonIgnore

+ 3 - 0
mjava/src/main/java/com/malk/base/JpaMap.java

@@ -27,6 +27,9 @@ import java.util.Map;
  * * @Query(value = "select * from vwpbCommonDataOrderPlan where if (?1 is not null, dPlanDate >= ?1, 1=1) and if (?2 is not null, dPlanDate <= ?2, 1=1) and sOrderNo like LTRIM('%' + ?3 + '%')", nativeQuery = true)
  * * Page<Map> queryOrderPlan(Date start, Date end, String sOrderNo, Pageable pageable);
  * 2. Specification, 谓词需要依赖于实体 [参考示例, 如下 test 方法实现]
+ * - 说明
+ * 1. @Query: nativeQuery查询使用表名, 不能使用对象名
+ * 2. ppExt: 现有表如u8, 不继承 BaseDto, 避免默认id与时间字段匹配异常
  */
 
 @Entity

+ 20 - 8
mjava/src/main/java/com/malk/server/common/McPage.java

@@ -1,7 +1,10 @@
 package com.malk.server.common;
 
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
-import org.springframework.data.domain.PageRequest;
+import lombok.NoArgsConstructor;
+import org.springframework.data.domain.Page;
 
 import java.util.List;
 
@@ -9,20 +12,29 @@ import java.util.List;
  * 分页集合数据结构
  */
 @Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
 public class McPage {
 
-    private int page;
+    private int page = 1;
 
     private int size;
 
-    private long total;
+    private long total = 0;
 
     private List list;
 
-    public McPage(PageRequest page, long total, List list) {
-        this.page = page.getPageNumber() + 1;
-        this.size = page.getPageSize();
-        this.total = total;
-        this.list = list;
+    public static McPage page(Page page) {
+        return page(page, page.getContent());
+    }
+
+    public static McPage page(Page page, List dataList) {
+        return McPage.builder()
+                .total(page.getTotalElements())
+                .page(page.getNumber())
+                .size(page.getSize())
+                .list(dataList)
+                .build();
     }
 }

+ 5 - 0
mjava/src/main/java/com/malk/server/dingtalk/DDR_New.java

@@ -27,6 +27,11 @@ public class DDR_New<T> extends VenR {
 
     private T result;
 
+    /**
+     * 离职记录列表
+     */
+    private List<Map<String, String>> records;
+
     /**
      * 审批实例id
      */

+ 16 - 1
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Attendance.java

@@ -56,7 +56,7 @@ public interface DDClient_Attendance {
     List<Map> listScheduleUsers(String access_token, String op_user_id, List<String> userids, long from_date_time, long to_date_time);
 
     /**
-     * 获取考勤报表列定义 [获取假期相关字段信息,不返回ID。如果希望获取假期相关信息,请调用获取报表假期数据接口]
+     * 获取考勤报表列定义 [获取假期相关字段信息,不返回ID。可设置考勤字段规则, 对于接口和钉钉后台考勤统计均生效]
      */
     List<Map> getAttColumns(String access_token);
 
@@ -80,4 +80,19 @@ public interface DDClient_Attendance {
      * @param to_date     结束时间,结束时间减去开始时间必须在31天以内 [格式为 yyyy-MM-dd HH:mm:ss]
      */
     List<Map> getLeaveTimeByNames(String access_token, String userid, List<String> leave_names, String from_date, String to_date);
+
+    /**
+     * 获取班次详情 [应出勤天数, 仅返回当天]
+     */
+    Map getAttendanceShiftDetail(String access_token, String op_user_id, String shift_id);
+
+    /**
+     * 搜索考勤组详情
+     */
+    Map getAttendanceGroupDetail(String access_token, String op_user_id, String group_id);
+
+    /**
+     * 搜索考勤组摘要
+     */
+    List<Map> getAttendanceGroupSearch(String access_token, String op_user_id, String group_name);
 }

+ 15 - 1
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Contacts.java

@@ -1,5 +1,6 @@
 package com.malk.service.dingtalk;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -36,7 +37,7 @@ public interface DDClient_Contacts {
     List<String> listDepartmentUserId(String access_token, long dept_id);
 
     /**
-     * 查询用户详情
+     * 查询用户详情 [如入职时间, 需要再=在花名册添加员工可见, 接口才会返回]
      */
     Map getUserInfoById(String access_token, String userId);
 
@@ -50,6 +51,19 @@ public interface DDClient_Contacts {
      */
     boolean deleteUser(String access_token, String userId);
 
+    /**
+     * 查询离职记录列表
+     *
+     * @param startTime 开始时间: 格式:YYYY-MM-DDTHH:mm:ssZ(ISO 8601/RFC 3339)
+     * @param extInfo   maxResults, 每页最大条目数,最大值50. nextToken 分页游标. endTime 结束时间, 默认当前时间, 查询跨度不能超过365天
+     */
+    List<Map<String, String>> getLeaveEmployeeRecords(String access_token, Date startTime, Map extInfo);
+
+    /**
+     * 获取指定用户的所有父部门列表
+     */
+    Map listParentByUser(String access_token, String userId);
+
     /**
      * 创建用户 [24小时只能收到一次邀请通知, 若是频繁退出再进入, 可关闭邀请确认后自动加入, 路径: 设置 - 安全中心 - 隐私开关 - 团队添加我时需要我的确认 - 关闭]
      */

+ 10 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDService.java

@@ -21,4 +21,14 @@ public interface DDService {
      * 判断员工是否在指定部门
      */
     boolean matchDepartment(String access_token, String userId, List<Long> deptIds);
+
+    /**
+     * 获取员工所属部门层级路径 [一个人存在多个部门默认取第一个, 不包含第一层部门]
+     */
+    List<Map> getUserDepartmentHierarchy(String access_token, String userId);
+
+    /**
+     * 获取员工所属部门层级路径 [名称拼接]
+     */
+    String getUserDepartmentHierarchyJoin(String access_token, String userId, String jon);
 }

+ 35 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Attendance.java

@@ -156,4 +156,39 @@ public class DDImplClient_Attendance implements DDClient_Attendance {
         DDR ddr = DDR.doPost("https://oapi.dingtalk.com/topapi/attendance/getleavetimebynames", null, DDConf.initTokenParams(access_token), boyds);
         return (List<Map>) ((Map) (ddr.getResult())).get("columns");
     }
+
+    /**
+     * 获取班次详情
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/shift-query
+     */
+    @Override
+    public Map getAttendanceShiftDetail(String access_token, String op_user_id, String shift_id) {
+        Map boyds = UtilMap.map("op_user_id, shift_id", op_user_id, shift_id);
+        return (Map) DDR.doPost("https://oapi.dingtalk.com/topapi/attendance/shift/query", null, DDConf.initTokenParams(access_token), boyds).getResult();
+    }
+
+    /**
+     * 查询企业考勤排班详情
+     */
+
+    /**
+     * @apiNote https://open.dingtalk.com/document/isvapp/query-a-single-attendance-group
+     */
+    @Override
+    public Map getAttendanceGroupDetail(String access_token, String op_user_id, String group_id) {
+        Map boyds = UtilMap.map("op_user_id, group_id", op_user_id, group_id);
+        return (Map) DDR.doPost("https://oapi.dingtalk.com/topapi/attendance/group/query", null, DDConf.initTokenParams(access_token), boyds).getResult();
+    }
+
+    /**
+     * 搜索考勤组摘要
+     *
+     * @apiNote https://open.dingtalk.com/document/isvapp/query-a-single-attendance-group
+     */
+    @Override
+    public List<Map> getAttendanceGroupSearch(String access_token, String op_user_id, String group_name) {
+        Map boyds = UtilMap.map("op_user_id, group_name", op_user_id, group_name);
+        return (List<Map>) DDR.doPost("https://oapi.dingtalk.com/topapi/attendance/group/search", null, DDConf.initTokenParams(access_token), boyds).getResult();
+    }
 }

+ 34 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Contacts.java

@@ -3,13 +3,16 @@ package com.malk.service.dingtalk.impl;
 import cn.hutool.core.util.ObjectUtil;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.server.dingtalk.DDR;
+import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.utils.UtilDateTime;
 import com.malk.utils.UtilList;
 import com.malk.utils.UtilMap;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -124,6 +127,37 @@ public class DDImplClient_Contacts implements DDClient_Contacts {
         return true;
     }
 
+    /**
+     * 查询离职记录列表
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/query-the-details-of-employees-who-have-left-office
+     */
+    @Override
+    public List<Map<String, String>> getLeaveEmployeeRecords(String access_token, Date startTime, Map extInfo) {
+        String start = UtilDateTime.format(startTime, "yyyy-MM-dd'T'HH:mm:ss") + "Z"; // 字符格式
+        Map body = UtilMap.map("startTime", start);
+        if (ObjectUtil.isNotNull(extInfo)) {
+            body.putAll(extInfo);
+            if (body.containsKey("endTime")) {
+                Date end = (Date) extInfo.get("endTime");
+                body.put("endTime", UtilDateTime.format(end, "yyyy-MM-dd'T'HH:mm:ss") + "Z");
+            }
+        }
+        if (!body.containsKey("maxResults")) {
+            body.put("maxResults", 50);
+        }
+        return DDR_New.doGet("https://api.dingtalk.com/v1.0/contact/empLeaveRecords", DDConf.initTokenHeader(access_token), body).getRecords();
+    }
+
+    /**
+     * 获取指定用户的所有父部门列表
+     */
+    @Override
+    public Map listParentByUser(String access_token, String userId) {
+        DDR r = DDR.doPost("https://oapi.dingtalk.com/topapi/v2/department/listparentbyuser", null, DDConf.initTokenParams(access_token), UtilMap.map("userid", userId));
+        return (Map) r.getResult();
+    }
+
     /**
      * 创建用户
      *

+ 28 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplService.java

@@ -16,6 +16,7 @@ import org.springframework.stereotype.Service;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 
 @Service
 @Slf4j
@@ -104,4 +105,31 @@ public class DDImplService implements DDService {
         }
         return isMatch;
     }
+
+    /**
+     * 获取员工所属部门全路径
+     */
+    @Override
+    public List<Map> getUserDepartmentHierarchy(String access_token, String userId) {
+        // PRD: 一个人存在多个部门默认取第一个
+        List<Number> deptIdList = (List<Number>) ((List<Map>) ddClient_contacts.listParentByUser(access_token, userId).get("parent_list")).get(0).get("parent_dept_id_list");
+        List<Map> deptInfo = new ArrayList();
+        // Number 仅仅作为数据类型声明, 避免比较类型不一致导致判定问题
+        for (Number deptId : deptIdList) {
+            if (deptId.longValue() == DDConf.TOP_DEPARTMENT) {
+                continue;
+            }
+            deptInfo.add(UtilMap.map("id, name", deptId, ddClient_contacts.getDepartmentInfo(access_token, deptId.longValue()).get("name")));
+        }
+        Collections.reverse(deptInfo);
+        return deptInfo;
+    }
+
+    /**
+     * 获取员工所属部门路径层级 [名称拼接]
+     */
+    @Override
+    public String getUserDepartmentHierarchyJoin(String access_token, String userId, String delimiter) {
+        return String.join(delimiter, getUserDepartmentHierarchy(access_token, userId).stream().map(dept -> dept.get("name").toString()).collect(Collectors.toList()));
+    }
 }

+ 10 - 0
mjava/src/main/java/com/malk/utils/UtilDateTime.java

@@ -35,6 +35,16 @@ public abstract class UtilDateTime {
         return Date.from(localDateTime.toInstant(ZoneOffset.of("+8")));
     }
 
+    // 在start之前, 兼容等于
+    public static boolean beforeAndEqual(Date start, Date compare) {
+        return start.before(compare) || start.equals(compare);
+    }
+
+    // 在end之后, 兼容等于
+    public static boolean afterAndEqual(Date end, Date compare) {
+        return end.after(compare) || end.equals(compare);
+    }
+
     // 获取时间段内小时
     public static float betweenHour(Temporal startInclusive, Temporal endExclusive) {
         return Duration.between(startInclusive, endExclusive).toMillis() / 60f;

+ 23 - 5
mjava/src/main/java/com/malk/utils/UtilExcel.java

@@ -2,6 +2,8 @@ package com.malk.utils;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.write.metadata.WriteSheet;
 import com.google.common.base.Strings;
 import com.malk.server.common.McException;
 import com.malk.server.common.McREnum;
@@ -210,16 +212,32 @@ public class UtilExcel {
     /////////////////// EasyExcel ///////////////////
 
     /**
-     * 导出功能 EasyExcel [假期余额导入模板 & 导入失败记录导出]
+     * 导出功能 EasyExcel [class支持map]
      * -
      * - Mac下, 若报错 Times 字体找不到, 下载安装一下, 不影响功能使用
+     * - ClassPathResource, 需要打包/编译后才能访问到. 识别不是架包内内容
      */
     @SneakyThrows
-    public static void exportBalanceWithHolidayTemplate(HttpServletResponse response, List dataList, Class dtoClass, @Nullable String fileName) {
-        InputStream inputStream = UtilFile.readPackageResource("templates/HolidayTemp.xlsx");
+    public static void exportListByTemplate(HttpServletResponse response, List dataList, Class dtoClass, @Nullable String fileName, String templateName) {
+        InputStream inputStream = UtilFile.readPackageResource("templates/" + templateName);
         UtilExcel.setResponseHeader(response, fileName, ".xlsx");
-        // 如果不用模板的方式导出的话,是doWrite
-        // EasyExcel.write(response.getOutputStream()).head(OFBalanceDTO.class).excelType(ExcelTypeEnum.XLSX).sheet().doWrite(list);
+        // 模板导出
         EasyExcel.write(response.getOutputStream(), dtoClass).withTemplate(inputStream).sheet().doFill(dataList);
     }
+
+    /**
+     * 列表与主表进行填充 [格式: 模板主表 {字段}, 列表 {.字段}]
+     */
+    @SneakyThrows
+    public static void exportMapAndListByTemplate(HttpServletResponse response, Object dataMain, List dataList, Class dtoClass, @Nullable String fileName, String templateName) {
+        InputStream inputStream = UtilFile.readPackageResource("templates/" + templateName);
+        UtilExcel.setResponseHeader(response, fileName, ".xlsx");
+        ExcelWriter workBook = EasyExcel.write(response.getOutputStream(), dtoClass).withTemplate(inputStream).build();
+        WriteSheet sheet = EasyExcel.writerSheet().build();
+        // 先单组数据填充,再多组数据填充
+        workBook.fill(dataMain, sheet);
+        workBook.fill(dataList, sheet);
+        workBook.finish();
+    }
+
 }

+ 1 - 1
mjava/src/main/java/com/malk/utils/UtilFile.java

@@ -138,7 +138,7 @@ public abstract class UtilFile {
     /**
      * 项目包文件, 读取 [服务Excel导出]
      * [ppExt]
-     * 1. ClassPathResource, 需要打包后才能访问到. 识别不是架包内内容
+     * 1. ClassPathResource, 需要打包/编译后才能访问到. 识别不是架包内内容
      * 2. 路径若 WebConfiguration 配置, 可使用配置别名
      * 3. 若是读取本地json, 文件不能使用 .josn 后缀, 会被转义导致解析异常
      */

+ 47 - 4
mjava/src/main/java/com/malk/utils/UtilList.java

@@ -2,9 +2,8 @@ package com.malk.utils;
 
 import cn.hutool.core.util.ObjectUtil;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 public abstract class UtilList {
 
@@ -35,11 +34,55 @@ public abstract class UtilList {
         return list[list.length - 1];
     }
 
-    // Arrays.asList 不可变, asList 为可变 [Set -> new ArrayList<>(Map.keySet())]
+    /**
+     * Arrays.asList 不可变, asList 为可变 [Set -> new ArrayList<>(Map.keySet())]
+     */
     public static List asList(Object... a) {
         List tList = new ArrayList<>();
         tList.addAll(Arrays.asList(a));
         return tList;
     }
 
+    /// collection.frequency方法,可以统计出某个对象在collection中出现的次数
+    private static Map _frequency(List list) {
+        Set uniqueWords = new HashSet<>(list);
+        int max = 0;
+        Object val = null;
+
+        for (Object word : uniqueWords) {
+            int num = Collections.frequency(list, word);
+            if (num > max) {
+                max = num;
+                val = word;
+            }
+        }
+        return UtilMap.map("max, val", max, val);
+    }
+
+    /**
+     * 某个对象在collection中出现最多次对象
+     */
+    public static Object maxFrequencyObject(List list) {
+        return _frequency(list).get("val");
+    }
+
+    /**
+     * 某个对象在collection中出现最多次次数
+     */
+    public static Object maxFrequencyCounty(List list) {
+        return _frequency(list).get("max");
+    }
+
+    /**
+     * 忽略集合内, map属性为0字段 [列表/导出]
+     */
+    public static List<Map> ignoreListMapZero(List<Map> list) {
+        return list.stream().map(item -> {
+            Map data = new HashMap();
+            for (Object key : item.keySet()) {
+                UtilMap.putNotZero(data, key.toString(), item.get(key));
+            }
+            return data;
+        }).collect(Collectors.toList());
+    }
 }

+ 19 - 1
mjava/src/main/java/com/malk/utils/UtilMap.java

@@ -2,12 +2,14 @@ package com.malk.utils;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.malk.server.common.McException;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
+@Slf4j
 public abstract class UtilMap {
 
     /************* 快速创建map ************/
@@ -71,7 +73,7 @@ public abstract class UtilMap {
     /**
      * 赋值 [为空对象, 字符串空null, 忽略]
      */
-    public static Map put(Map data, String key, Object value) {
+    public static Map putNotNull(Map data, String key, Object value) {
         if (ObjectUtil.isNull(data)) {
             data = new HashMap();
         }
@@ -81,6 +83,22 @@ public abstract class UtilMap {
         return data;
     }
 
+    /**
+     * 赋值 [值为0, 忽略]
+     */
+    public static Map putNotZero(Map data, String key, Object value) {
+        float val = 0.f;
+        try {
+            val = Float.valueOf(String.valueOf(value));
+        } catch (Exception e) {
+            data.put(key, value);
+        }
+        if (val != 0.f) {
+            data.put(key, value);
+        }
+        return data;
+    }
+
     /************* 取值 ************/
 
     /**

+ 13 - 3
mjava/src/main/java/com/malk/utils/UtilNumber.java

@@ -51,14 +51,24 @@ public class UtilNumber {
     }
 
     // 小数位精度格式
-    public static final String formatPrecision(double number) {
+    public static final String formatPrecisionString(double number) {
         DecimalFormat df = new DecimalFormat("#.00");
         return df.format(new BigDecimal(number));
     }
 
     // 小数位精度格式
-    public static final String formatPrecision(float number) {
-        return formatPrecision(Double.valueOf(String.valueOf(number)));
+    public static final double formatPrecisionValue(double number) {
+        return Double.valueOf(formatPrecisionString(number));
+    }
+
+    // 小数位精度格式
+    public static final double formatPrecisionValue(float number) {
+        return Double.valueOf(formatPrecisionString(number));
+    }
+
+    // 小数位精度格式
+    public static final String formatPrecisionString(float number) {
+        return formatPrecisionString(Double.valueOf(String.valueOf(number)));
     }
 
     // 非数值型字符串, 空字符串兼容

+ 0 - 120
mjava/target/classes/META-INF/spring-configuration-metadata.json

@@ -35,26 +35,6 @@
       "type": "com.malk.server.common.FilePath$Path",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
     {
       "name": "file.source",
       "type": "com.malk.server.common.FilePath$Source",
@@ -65,26 +45,6 @@
       "type": "com.malk.server.common.FilePath$Source",
       "sourceType": "com.malk.server.common.FilePath$Source"
     },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
     {
       "name": "fxiaoke",
       "type": "com.malk.server.fxiaoke.FXKConf",
@@ -209,96 +169,16 @@
       "type": "java.lang.String",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
     {
       "name": "file.path.image",
       "type": "java.lang.String",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
     {
       "name": "file.path.tmp",
       "type": "java.lang.String",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
     {
       "name": "file.source.fonts",
       "type": "java.lang.String",

+ 1 - 0
pom.xml

@@ -20,6 +20,7 @@
         <module>mjava-gewu</module>
         <module>mjava-shangfeng</module>
         <module>mjava-pake</module>
+        <module>mjava-yangu</module>
     </modules>
     <packaging>pom</packaging>