diff --git a/cash-api/market-server/src/main/java/com/czg/controller/admin/AttendanceController.java b/cash-api/market-server/src/main/java/com/czg/controller/admin/AttendanceController.java new file mode 100644 index 000000000..7aad1b443 --- /dev/null +++ b/cash-api/market-server/src/main/java/com/czg/controller/admin/AttendanceController.java @@ -0,0 +1,62 @@ +package com.czg.controller.admin; + +import cn.hutool.core.bean.BeanUtil; +import com.czg.account.entity.ShopConfig; +import com.czg.account.service.ShopConfigService; +import com.czg.market.dto.AttendanceConfigDTO; +import com.czg.market.service.AttendanceService; +import com.czg.market.vo.DingAttendanceStatsVO; +import com.czg.resp.CzgResult; +import com.czg.sa.StpKit; +import jakarta.annotation.Resource; +import org.apache.dubbo.config.annotation.DubboReference; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; + +/** + * 考勤打卡数据 + * @author Administrator + */ +@RestController +@RequestMapping("/admin/attendance") +public class AttendanceController { + @Resource + private AttendanceService attendanceService; + @DubboReference + private ShopConfigService shopConfigService; + + /** + * 配置获取 + * @return 配置信息 + */ + @GetMapping("/config") + public CzgResult getConfig() { + return CzgResult.success(shopConfigService.getById(StpKit.USER.getShopId())); + } + + /** + * 修改配置 + */ + @PutMapping("/config") + public CzgResult editConfig(@Validated @RequestBody AttendanceConfigDTO dto) { + return CzgResult.success(shopConfigService.editConfig(StpKit.USER.getShopId(), BeanUtil.copyProperties(dto, ShopConfig.class))); + } + + + /** + * 列表 + * @param page 开始页码 + * @param size 数量 + * @param name 姓名 + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + @GetMapping + public CzgResult> getList(@RequestParam Integer page, @RequestParam Integer size, + @RequestParam(required = false) String name, @RequestParam(required = false) String startTime, + @RequestParam(required = false) String endTime) { + return CzgResult.success(attendanceService.list(StpKit.USER.getShopId(), page, size, name, startTime, endTime)); + } +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/account/service/ShopConfigService.java b/cash-common/cash-common-service/src/main/java/com/czg/account/service/ShopConfigService.java index b7575e4e3..f180699d4 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/account/service/ShopConfigService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/account/service/ShopConfigService.java @@ -15,4 +15,6 @@ import java.util.List; public interface ShopConfigService extends IService { void editStatusByShopIdList(Long mainShopId, Integer isEnable, boolean onyUpValid, String name, String useShopType, List shopIdList); + + boolean editConfig(Long shopId, ShopConfig shopConfig); } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/market/dto/AttendanceConfigDTO.java b/cash-common/cash-common-service/src/main/java/com/czg/market/dto/AttendanceConfigDTO.java new file mode 100644 index 000000000..65ebd68c5 --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/market/dto/AttendanceConfigDTO.java @@ -0,0 +1,12 @@ +package com.czg.market.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class AttendanceConfigDTO { + @NotBlank + private String dingAppKey; + @NotBlank + private String dingAppSecret; +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/market/service/AttendanceService.java b/cash-common/cash-common-service/src/main/java/com/czg/market/service/AttendanceService.java new file mode 100644 index 000000000..b4ddd39e2 --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/market/service/AttendanceService.java @@ -0,0 +1,9 @@ +package com.czg.market.service; + +import com.czg.market.vo.DingAttendanceStatsVO; + +import java.util.ArrayList; + +public interface AttendanceService { + ArrayList list(Long shopId, Integer page, Integer size, String name, String startTime, String endTime); +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingAttendanceStatsVO.java b/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingAttendanceStatsVO.java new file mode 100644 index 000000000..fe69c8400 --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingAttendanceStatsVO.java @@ -0,0 +1,60 @@ +package com.czg.market.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class DingAttendanceStatsVO { + /** 出勤天数 */ + private String attendDays; + + /** 休息天数 */ + private String restDays; + + /** 工作时长(小时/分钟,可自行约定) */ + private String workDuration; + + /** 迟到次数 */ + private String lateCount; + + /** 迟到时长(分钟) */ + private String lateDuration; + + /** 应出勤天数 */ + private String shouldAttendDays; + + /** + * 用户名 + */ + private String name; + private String userId; + /** + * 用户是否有有效考勤数据 + */ + private boolean isActive; + + public void setValByName(String name, String val) { + switch (name) { + case "出勤天数": + this.attendDays = val; + break; + case "休息天数": + this.restDays = val; + break; + case "工作时长": + this.workDuration = val; + break; + case "迟到次数": + this.lateCount = val; + break; + case "迟到时长": + this.lateDuration = val; + break; + case "应出勤天数": + this.shouldAttendDays = val; + break; + } + } + +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingUserVO.java b/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingUserVO.java new file mode 100644 index 000000000..c3ac28d5c --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/market/vo/DingUserVO.java @@ -0,0 +1,73 @@ +package com.czg.market.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class DingUserVO{ + /** + * 部门排序号(JSON 字段:dept_order) + */ + private Long deptOrder; + + /** + * 是否为部门负责人(JSON 字段:leader) + */ + private Boolean leader; + + /** + * 是否为老板(JSON 字段:boss) + */ + private Boolean boss; + + /** + * 钉钉统一标识(JSON 字段:unionid) + */ + private String unionid; + + /** + * 是否为专属账号(JSON 字段:exclusive_account) + */ + private Boolean exclusiveAccount; + + /** + * 账号是否激活(JSON 字段:active) + */ + private Boolean active; + + /** + * 是否为管理员(JSON 字段:admin) + */ + private Boolean admin; + + /** + * 头像 URL(JSON 字段:avatar) + */ + private String avatar; + + /** + * 是否隐藏手机号(JSON 字段:hide_mobile) + */ + private Boolean hideMobile; + + /** + * 职位(JSON 字段:title) + */ + private String title; + + /** + * 钉钉用户 ID(JSON 字段:userid) + */ + private String userid; + + /** + * 用户名(JSON 字段:name) + */ + private String name; + + /** + * 所属部门 ID 列表(JSON 字段:dept_id_list) + */ + private List deptIdList; +} diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java index 5b062a397..a0d4a674b 100644 --- a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java +++ b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/ShopConfigServiceImpl.java @@ -1,5 +1,6 @@ package com.czg.service.account.service.impl; +import cn.hutool.core.bean.BeanUtil; import com.czg.account.dto.ShopConfigDTO; import com.czg.account.entity.ShopConfig; import com.czg.account.entity.ShopInfo; @@ -50,6 +51,24 @@ public class ShopConfigServiceImpl extends ServiceImpl shopIdList) { ShopConfig shopConfig = getById(mainShopId); diff --git a/cash-service/market-service/pom.xml b/cash-service/market-service/pom.xml index 8120d1b9d..97577907d 100644 --- a/cash-service/market-service/pom.xml +++ b/cash-service/market-service/pom.xml @@ -22,6 +22,18 @@ pay-service 1.0.0 + + + com.aliyun + dingtalk + 2.2.40 + + + + com.aliyun + alibaba-dingtalk-service-sdk + 2.0.0 + diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AttendanceServiceImpl.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AttendanceServiceImpl.java new file mode 100644 index 000000000..fde20c32f --- /dev/null +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/AttendanceServiceImpl.java @@ -0,0 +1,36 @@ +package com.czg.service.market.service.impl; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.czg.market.service.AttendanceService; +import com.czg.market.vo.DingAttendanceStatsVO; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; + +@Service +public class AttendanceServiceImpl implements AttendanceService { + @Resource + private DingService dingService; + + @Override + public ArrayList list(Long shopId, Integer page, Integer size, String name, String startTime, String endTime) { + DateTime start; + DateTime end; + if (StrUtil.isBlank(startTime)) { + start = DateUtil.beginOfMonth(new Date()); + }else { + start = DateUtil.parse(startTime); + } + + if (StrUtil.isBlank(endTime)) { + end = DateUtil.date(); + }else { + end = DateUtil.parse(endTime); + } + return dingService.getUserReport(start, end, name); + } +} diff --git a/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/DingService.java b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/DingService.java new file mode 100644 index 000000000..7be432718 --- /dev/null +++ b/cash-service/market-service/src/main/java/com/czg/service/market/service/impl/DingService.java @@ -0,0 +1,187 @@ +package com.czg.service.market.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSONObject; +import com.czg.market.vo.DingAttendanceStatsVO; +import com.czg.market.vo.DingUserVO; +import com.czg.service.RedisService; +import com.dingtalk.api.DefaultDingTalkClient; +import com.dingtalk.api.DingTalkClient; +import com.dingtalk.api.request.*; +import com.dingtalk.api.response.*; +import com.taobao.api.ApiException; +import com.taobao.api.TaobaoResponse; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class DingService { + @Resource + private RedisService redisService; + + + public String getToken() { + Object accessToken = redisService.get("ding_access_token"); + if (accessToken instanceof String && StrUtil.isNotBlank((String) accessToken)) { + return (String) accessToken; + } + DingTalkClient client = new DefaultDingTalkClient("https://api.dingtalk.com/gettoken"); + OapiGettokenRequest req = new OapiGettokenRequest(); + req.setAppkey("ding9bqzj4890z81o3y2"); + req.setAppsecret("PiRU2CQ_KrZH104UoULeBfSbFw4YhPI0fZZC_bUr1aubvbsP6zmMv7nC1xxF9QBS"); + req.setHttpMethod("GET"); + OapiGettokenResponse rsp; + try { + rsp = client.execute(req); + } catch (ApiException e) { + throw new RuntimeException(e); + } + System.out.println(rsp.getBody()); + JSONObject data = getData(rsp, true); + Long expiresIn = data.getLong("expires_in"); + redisService.set("ding_access_token", data.getString("access_token"), expiresIn - 100); + return data.getString("access_token"); + } + + public Map getColumns() { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/attendance/getattcolumns"); + OapiAttendanceGetattcolumnsRequest req = new OapiAttendanceGetattcolumnsRequest(); + OapiAttendanceGetattcolumnsResponse rsp = null; + try { + String token = getToken(); + rsp = client.execute(req, token); + } catch (ApiException e) { + throw new RuntimeException(e); + } + + JSONObject jsonObject = JSONObject.parseObject(rsp.getBody()); + if (jsonObject.getInteger("errcode") != 0) { + throw new RuntimeException(jsonObject.getString("errmsg")); + } + HashMap map = new HashMap<>(); + Map columMap = jsonObject.getJSONObject("result").getJSONArray("columns").stream() + .filter(item -> { + JSONObject item1 = (JSONObject) item; + return item1.containsKey("id") && CollUtil.newArrayList("出勤天数", "休息天数", "工作时长", "迟到次数", "迟到时长", "应出勤天数").contains(item1.getString("name")); + }) + .collect(Collectors.toMap(item -> ((JSONObject) item).getLong("id").toString(), item -> ((JSONObject) item).getString("name"))); + + return columMap; + } + + public static JSONObject getData(TaobaoResponse response, boolean checkNull) { + if (!"0".equals(response.getErrorCode())) { + Map map = BeanUtil.beanToMap(response); + throw new RuntimeException(map.get("errmsg").toString()); + } + String body = response.getBody(); + if (body == null) { + if (checkNull) { + throw new RuntimeException("body is null"); + } + return null; + } + JSONObject jsonObject = JSONObject.parseObject(body); + if (jsonObject.getInteger("errcode") != 0) { + throw new RuntimeException(jsonObject.getString("errmsg")); + } + return jsonObject; + } + + public List getUserByDeptId(Long deptId) throws ApiException { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/list"); + OapiV2UserListRequest req = new OapiV2UserListRequest(); + req.setDeptId(deptId); + req.setCursor(0L); + req.setSize(50L); + req.setOrderField("modify_desc"); + req.setContainAccessLimit(true); + req.setLanguage("zh_CN"); + OapiV2UserListResponse rsp = client.execute(req, getToken()); + JSONObject data = getData(rsp, true); + List userVOList = data.getJSONObject("result").getJSONArray("list").toJavaList(DingUserVO.class); + return userVOList; + } + + public JSONObject getRepost(Date startTime, Date endTime, String userId, String columIdList) throws ApiException { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/attendance/getcolumnval"); + OapiAttendanceGetcolumnvalRequest req = new OapiAttendanceGetcolumnvalRequest(); + req.setUserid(userId); + req.setColumnIdList(columIdList); + req.setFromDate(startTime); + req.setToDate(endTime); + OapiAttendanceGetcolumnvalResponse rsp = client.execute(req, getToken()); + JSONObject data = getData(rsp, false); + return data == null ? null : data.getJSONObject("result"); + } + + public ArrayList getUserList(String name) { + try { + DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/department/listsub"); + OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest(); + req.setDeptId(1L); + req.setLanguage("zh_CN"); + OapiV2DepartmentListsubResponse rsp = client.execute(req, getToken()); + Set deptIdList = getData(rsp, true).getJSONArray("result").stream().map(item -> ((JSONObject) item) + .getLong("dept_id")).collect(Collectors.toSet()); + + ArrayList dingUserVOS = new ArrayList<>(); + for (Long dptId : deptIdList) { + dingUserVOS.addAll(getUserByDeptId(dptId)); + } + if (StrUtil.isNotBlank(name)) { + return dingUserVOS.stream().filter(item -> item.getName().contains(name)).collect(Collectors.toCollection(ArrayList::new)); + } + return dingUserVOS; + }catch (Exception e) { + throw new RuntimeException(e); + } + } + + public ArrayList getUserReport(DateTime startTime, DateTime endTime, String name) { + Map columns = getColumns(); + ArrayList userList = getUserList(name); + ArrayList statsVOS = new ArrayList<>(); + for (DingUserVO item : userList) { + JSONObject repost = null; + try { + repost = getRepost(startTime, endTime, item.getUserid(), + String.join(",", columns.keySet())); + } catch (ApiException e) { + throw new RuntimeException(e); + } + DingAttendanceStatsVO statsVO = new DingAttendanceStatsVO(); + statsVOS.add(statsVO); + statsVO.setUserId(item.getUserid()); + statsVO.setName(item.getName()); + if (repost != null) { + statsVO.setActive(true); + repost.getJSONArray("column_vals").forEach(info -> { + JSONObject infoObj = (JSONObject) info; + Long id = infoObj.getJSONObject("column_vo").getLong("id"); + + + BigDecimal value = BigDecimal.ZERO; + for (Object column : infoObj.getJSONArray("column_vals")) { + JSONObject columnObj = (JSONObject) column; + value = value.add(new BigDecimal(columnObj.getString("value"))); + } + statsVO.setValByName(columns.get(id.toString()), value.stripTrailingZeros().toPlainString()); + }); + } + } + + return statsVOS; + } + + static void main() throws ApiException { + } +}