钉钉考勤

This commit is contained in:
张松
2025-12-01 11:25:40 +08:00
parent 824f35e636
commit 001607b649
10 changed files with 472 additions and 0 deletions

View File

@@ -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<ShopConfig> getConfig() {
return CzgResult.success(shopConfigService.getById(StpKit.USER.getShopId()));
}
/**
* 修改配置
*/
@PutMapping("/config")
public CzgResult<Boolean> 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<ArrayList<DingAttendanceStatsVO>> 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));
}
}

View File

@@ -15,4 +15,6 @@ import java.util.List;
public interface ShopConfigService extends IService<ShopConfig> {
void editStatusByShopIdList(Long mainShopId, Integer isEnable, boolean onyUpValid, String name, String useShopType, List<Long> shopIdList);
boolean editConfig(Long shopId, ShopConfig shopConfig);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
package com.czg.market.service;
import com.czg.market.vo.DingAttendanceStatsVO;
import java.util.ArrayList;
public interface AttendanceService {
ArrayList<DingAttendanceStatsVO> list(Long shopId, Integer page, Integer size, String name, String startTime, String endTime);
}

View File

@@ -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;
}
}
}

View File

@@ -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;
/**
* 头像 URLJSON 字段avatar
*/
private String avatar;
/**
* 是否隐藏手机号JSON 字段hide_mobile
*/
private Boolean hideMobile;
/**
* 职位JSON 字段title
*/
private String title;
/**
* 钉钉用户 IDJSON 字段userid
*/
private String userid;
/**
* 用户名JSON 字段name
*/
private String name;
/**
* 所属部门 ID 列表JSON 字段dept_id_list
*/
private List<Long> deptIdList;
}

View File

@@ -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<ShopConfigMapper, ShopCon
}
@Override
public boolean editConfig(Long shopId, ShopConfig shopConfig) {
ShopConfig info = getById(shopId);
if (info == null) {
info = new ShopConfig();
BeanUtil.copyProperties(shopConfig, info);
info.setId(shopId);
info.setMainId(shopInfoMapper.selectOneByQuery(new QueryWrapper().eq(ShopInfo::getId, shopId)).getMainId());
info.setId(shopId);
save(info);
}else {
BeanUtil.copyProperties(shopConfig, info);
updateById(info);
}
return true;
}
@Override
public void editStatusByShopIdList(Long mainShopId, Integer isEnable, boolean onyUpValid, String property, String useShopType, List<Long> shopIdList) {
ShopConfig shopConfig = getById(mainShopId);

View File

@@ -22,6 +22,18 @@
<artifactId>pay-service</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.2.40</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -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<DingAttendanceStatsVO> 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);
}
}

View File

@@ -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<String, String> 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<String, String> map = new HashMap<>();
Map<String, String> 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<String, Object> 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<DingUserVO> 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<DingUserVO> 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<DingUserVO> 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<Long> deptIdList = getData(rsp, true).getJSONArray("result").stream().map(item -> ((JSONObject) item)
.getLong("dept_id")).collect(Collectors.toSet());
ArrayList<DingUserVO> 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<DingAttendanceStatsVO> getUserReport(DateTime startTime, DateTime endTime, String name) {
Map<String, String> columns = getColumns();
ArrayList<DingUserVO> userList = getUserList(name);
ArrayList<DingAttendanceStatsVO> 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 {
}
}