一、注册高德地图。
应用管理创建应用,分别添加Andriod平台、Web服务、Web端、微信小程序四种类型的key。
二、考勤规则
打卡地点选择位置代码:
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref, watchEffect } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
const emit = defineEmits(["submitMarker"]);
const props = defineProps({
markForm: {
type: Object,
default: null
}
});
const fieldNames = { label: "name", value: "id" };
const loading = ref<boolean>(false);
const searchKey = ref<any>();
const options = ref<any>([]);
const autoComplete = ref<any>();
// 标记点
const marker = ref<any>();
// 位置信息
const form = reactive({
lng: "",
lat: "",
address: "",//详细地址
simpleAddress: "",//地址简称
//地区编码
adcode: "",
addressId: ""
});
const geoCoder = ref<any>();
const aMap = ref<any>();
const map = ref<any>();
watchEffect(() => {
if (props.markForm && map.value) {
form.lng = props.markForm.attendanceLongitude;
form.lat = props.markForm.attendanceLatitude;
form.address = props.markForm.attendanceAddress;
form.simpleAddress = props.markForm.simpleAddress;
form.addressId = props.markForm.attendanceAddressId;
// 清除点
removeMarker();
// 标记点
setMapMarker();
}
});
onMounted(() => {
window._AMapSecurityConfig = {
securityJsCode: "3a09232a7b75996c571a2a233211"
};
AMapLoader.load({
key: "6a352ddjnewewb2814bebf80091wewec38e9", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: ["AMap.Scale", "AMap.ToolBar", "AMap.Geolocation", "AMap.PlaceSearch", "AMap.Geocoder", "AMap.AutoComplete"] // 需要使用的的插件列表,如比例尺'AMap.Scale'等
})
.then((AMap) => {
aMap.value = AMap;
map.value = new AMap.Map("container", {
// 设置地图容器id
viewMode: "3D", // 是否为3D地图模式
zoom: 11, // 初始化地图级别
center: [116.397428, 39.90923] // 初始化地图中心点位置
});
geoCoder.value = new AMap.Geocoder({
city: "010", //城市设为北京,默认:“全国”
radius: 1000 //范围,默认:500
});
// 搜索提示插件
autoComplete.value = new AMap.AutoComplete({ city: "全国" });
})
.catch((e) => {
console.log(e);
});
});
// 标记点
function setMapMarker() {
// 自动适应显示想显示的范围区域
map.value.setFitView();
marker.value = new aMap.value.Marker({
map: map.value,
position: [form.lng, form.lat]
});
map.value.setFitView();
map.value.add(marker.value);
}
// 逆解析地址
function toGeoCoder() {
let lnglat = [form.lng, form.lat];
geoCoder.value.getAddress(lnglat, (status, result) => {
if (status === "complete" && result.regeocode) {
form.address = result.regeocode.formattedAddress;
}
});
}
// 清除点
function removeMarker() {
if (marker.value) {
map.value.remove(marker.value);
}
}
// 搜索
function handleChange(value) {
if (value) {
loading.value = true;
setTimeout(() => {
loading.value = false;
autoComplete.value.search(value, (status, result) => {
options.value = result.tips;
});
}, 200);
} else {
options.value = [];
}
}
function currentSelect(val, option) {
if (!val) {
return;
}
form.lng = option.location.lng;
form.lat = option.location.lat;
form.address = option.district + option.name;
form.simpleAddress = option.name;
form.adcode = option.adcode;
form.addressId = option.id;
// 清除点
removeMarker();
// 标记点
setMapMarker();
emit("submitMarker", form);
}
onUnmounted(() => {
map.value?.destroy();
});
</script>
<template>
<a-select
v-model:value="searchKey"
:options="options"
:filter-option="false"
label-in-value
show-search
@search="handleChange"
placeholder="请输入关键词"
:fieldNames="fieldNames"
@change="currentSelect"
></a-select>
<div v-if="form.simpleAddress" class="address">
已选位置:
<Icon icon="ant-design:environment-outlined"></Icon>
{{ form.simpleAddress }}
</div>
<div id="container"></div>
</template>
<style scoped>
#container {
width: 100%;
height: 500px;
margin-top: 10px;
}
.address {
margin: 10px 0;
color: #FFA500;
display: flex;
align-items: center
}
</style>
安装高德地图Web端(JS API):
npm install @amap/amap-jsapi-loader
securityJsCode:使用自己的安全密钥。key换为自己的。
三、uniapp 小程序
(1)获取打卡规则:上下班打卡时间、当前位置标记、考勤打卡范围,加载打卡记录,打卡距离计算是否外勤打卡,上班打卡还是下班打卡,上班是正常打卡或迟到打卡,下班是正常打卡或早退打卡等计算。打卡提交和更新打卡。节假日、工作日计算是否需要打卡。
非工作日也允许打卡,不做迟到和早退标记,时间自由,不做缺卡标记。
(2)微信小程序端申请开通获取位置接口。
配置文件勾选位置接口,填写接口申请原因。
确保配置文件中包含以下配置。
(3) 微信小程序打卡页面uni.getSetting获取定位权限,没有授权会弹窗授权。
(4)打卡
![]() |
上述页面代码如下:
<template>
<view class="container">
<StatusBar :offset="10" />
<view class="user-info">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar">{{userInfo.realname}}</view>
<view class="user-info">
<view class="name">{{userInfo.realname}}</view>
<view class="desc">{{currentDepartName}}</view>
</view>
<view class="statistics" @click="viewCalendar">
<view class="flex align-center">
<image src="/static/statistics.png" class='statistics-image' mode='aspectFit'></image>
<text class="statistics-text">打卡统计</text>
</view>
</view>
</view>
<!-- 打卡信息 -->
<view class="attendance-card">
<view class="attendance-row">
<view class="attendance-item">
<view class="title">
上班{{attendanceRule.startWorkTime || ''}}
</view>
<view class="text">
<text class="tag" v-if="startWorkRecord && startWorkRecord.inoutsideType ==2">外勤</text>
</view>
<view class="status">
<view class="checked" v-if="startWorkRecord && startWorkRecord.attendanceTime">
<image src="/static/checked.png" class='recommend-image' mode='aspectFit'></image>
<view class="margin-lf">
{{startWorkRecord.attendanceTime.substring(11,16) || ''}}已打卡
</view>
</view>
<text class="not-checked" v-else>未打卡</text>
<u-tag text="缺卡" type="warning" shape="circle" size="mini"
v-if="attendanceInfo && attendanceInfo.isStartMiss"></u-tag>
</view>
</view>
<view class="attendance-item">
<view class="title">
下班{{attendanceRule.endWorkTime || ''}}
</view>
<view class="text">
<text class="tag" v-if="endWorkRecord && endWorkRecord.inoutsideType ==2">外勤</text>
</view>
<view class="status">
<view class="checked" v-if="endWorkRecord && endWorkRecord.attendanceTime">
<image src="/static/checked.png" class='recommend-image' mode='aspectFit'></image>
<view class="margin-lf">{{endWorkRecord.attendanceTime.substring(11,16) || ''}}已打卡
</view>
</view>
<view class="not-checked" v-else>未打卡</view>
<u-tag text="缺卡" type="warning" shape="circle" size="mini"
v-if="attendanceInfo && attendanceInfo.isEndMiss"></u-tag>
</view>
</view>
</view>
<view class="margin-top-sm" v-if="!isNeedAttendance">
<u-alert description="今日休息" type="primary" show-icon></u-alert>
</view>
</view>
</view>
<view>
<map-positioning-punch :clock-in-area="clockInArea" :refresh-timeout="refreshTimeout"
@clockInClick="clockIn" :is-report="true" @change="locationChange" v-if="clockInArea.length > 0">
</map-positioning-punch>
<u-modal :show="showConfirm" @confirm="saveAttendance" title="提示" @cancel="showConfirm=false" ref="uModal"
:asyncClose="true" :showCancelButton="true" content="确定要早退打卡吗?"></u-modal>
</view>
</view>
</template>
<script>
import {
apiGetAttendanceRule,
apiSaveAttendance,
apiIsLeaveEarly,
apiGetCurrentDept,
apiListTodayAttendance
} from "@/common/http.api.js"
import {
mapState,
mapActions
} from 'vuex'
export default {
data() {
return {
attendanceTypeInfo: null,
showConfirm: false,
// 打卡区域设置
clockInArea: [],
// 刷新打卡区域频率
refreshTimeout: 15000,
params: {},
attendanceRule: {},
currentDepartName: "",
attendanceInfo: null,
startWorkRecord: null,
endWorkRecord: null,
remark: "",
showRemark: false,
isNeedAttendance: true
}
},
computed: {
...mapState(['loginState', 'userInfo']),
},
async onShow() {
const res = await apiGetAttendanceRule()
this.attendanceRule = res
this.clockInArea.push({
longitude: res.attendanceLongitude,
latitude: res.attendanceLatitude,
distance: res.allowCheckinRange,
})
if (!!this.loginState) {
const data = await apiGetCurrentDept()
this.currentDepartName = data.orgCodeTxt;
// #ifdef MP-WEIXIN
uni.getSetting({
success(res) {
if (!res.authSetting['scope.userLocation']) {
uni.authorize({
scope: 'scope.userLocation',
success() {},
fail() {
console.log('用户未授权');
}
});
}
}
});
// #endif
//获取今天的打卡数据
this.listTodayAttendance();
}
},
methods: {
//获取今天的打卡数据
async listTodayAttendance() {
this.attendanceInfo = await apiListTodayAttendance()
if (this.attendanceInfo) {
this.startWorkRecord = this.attendanceInfo.start;
this.endWorkRecord = this.attendanceInfo.end;
this.isNeedAttendance = this.attendanceInfo.isNeedAttendance
}
},
//提交打卡数据
async saveAttendance() {
this.showConfirm = false
this.loading = true
try {
await apiSaveAttendance(this.params)
uni.showToast({
icon: 'success',
title: '打卡成功'
})
//获取今天的打卡数据
this.listTodayAttendance();
} finally {
this.loading = false
}
},
// 位置变化
locationChange({
location,
areaLocation,
distance
}) {},
// 打卡回调事件
// location 当前位置,attendanceTypeInfo 考勤信息
async clockIn({
location,
attendanceTypeInfo,
addressName
}) {
this.attendanceTypeInfo = attendanceTypeInfo
this.params = {
attendanceAddress: addressName,
attendanceLongitude: location.longitude,
attendanceLatitude: location.latitude,
inoutsideType: attendanceTypeInfo.inoutsideType.code,
attendanceType: attendanceTypeInfo.attendanceType.code,
workType: attendanceTypeInfo.workType.code
}
if (attendanceTypeInfo.workType.code == 2) { //下班卡
const data = await apiIsLeaveEarly() //判断是否早退打卡
if (data) {
this.showConfirm = true
} else {
await this.saveAttendance()
}
} else {
await this.saveAttendance()
}
},
viewCalendar() {
uni.navigateTo({
url: '/subpages/attendancecalendar/attendancecalendar'
})
}
}
}
</script>
<style scoped>
.container {
background: #f7fafd;
min-height: 100vh;
}
.user-info {
padding: 20rpx;
}
.user-card {
position: relative;
display: flex;
align-items: center;
background: #fff;
border-radius: 20rpx;
padding: 30rpx 20rpx;
margin-bottom: 30rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
background: #4a90e2;
color: #fff;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
margin-right: 20rpx;
}
.user-info .name {
font-size: 32rpx;
font-weight: bold;
}
.user-info .desc {
font-size: 24rpx;
color: #888;
}
.attendance-card {
background: #fff;
border-radius: 20rpx;
margin-bottom: 10rpx;
}
.attendance-row {
display: flex;
justify-content: space-between;
}
.attendance-item {
position: relative;
width: 49%;
background-color: #EBEBEB;
padding: 30rpx 20rpx;
border-radius: 20rpx;
}
.title {
font-size: 32rpx;
margin-bottom: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.tag {
background: #4ec6a4;
color: #fff;
font-size: 24rpx;
border-radius: 8rpx;
padding: 5rpx 10rpx;
}
.status {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 26rpx;
}
.checked {
display: flex;
align-items: center;
color: #4a90e2;
margin-right: 10rpx;
}
.not-checked {
color: #747A7B;
}
.recommend-image {
width: 30rpx;
height: 30rpx;
}
.margin-lf {
margin-left: 10rpx;
}
.text {
position: absolute;
top: 0;
right: 0;
z-index: 9;
color: white;
font-size: 10px;
padding: 5px;
}
.statistics {
position: absolute;
top: 0;
right: 10px;
z-index: 9;
padding: 10px;
}
.statistics-image {
width: 40rpx;
height: 40rpx;
}
.statistics-text {
color: black;
font-size: 28rpx;
margin-left: 10rpx;
}
</style>
(5)打卡日历
根据考勤规则标记需要打卡的星期,是否自动过滤节假日。工作日缺卡红色标记,上下班均不缺卡做蓝色标记。选择日期后统计打卡次数,获取打卡时间和位置,外勤打卡标记。
![]() |
上述页面代码如下:
<template>
<view class="calendar">
<StatusBar :offset="10" />
<ren-calendar ref='ren' :markDays='markDays' :markBlueDays="markBlueDays" :headerBar='true'
@onDayClick='onDayClick' @onMonthClick="onMonthClick"></ren-calendar>
<view class="attendance-container">
<view class="shift-info">
<view>当日班次:
<text>固定上下班 行政班{{attendanceRule.startWorkTime || ''}}-{{attendanceRule.endWorkTime || ''}}</text>
</view>
<view>出勤统计:打卡{{checkinCount || 0}}次</view>
</view>
<view class="margin-top-sm margin-bottom-sm" v-if="currentAttendance && !currentAttendance.isWorkDay">
<u-alert description="休息日" type="primary" show-icon></u-alert>
</view>
<view class="timeline">
<view class="timeline-item" v-for="(item, idx) in records" :key="idx">
<view class="dot"></view>
<view>
<view class="time-row">
<text class="title">{{ item.workType == 1?'上班':'下班' }}</text>
<text v-if="item.note" class="margin-right-sm">{{ item.note || '' }}</text>
<u-tag text="缺卡" plain size="mini" type="warning"
v-if="item.workType == 1 && item.isStartMiss"></u-tag>
<u-tag text="缺卡" plain size="mini" type="warning"
v-if="item.workType == 2 && item.isEndMiss"></u-tag>
<text class="time"
v-if="item.attendanceTime">{{ item.attendanceTime.substring(11,16) || '' }}</text>
<text class="tag" v-if="item.inoutsideType ==2">外勤</text>
</view>
<view class="location" v-if="item.attendanceAddress">
<image src="/static/position.png" class="pos-image"></text>
{{ item.attendanceAddress }}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import RenCalendar from "@/subpages/components/ren-calendar/ren-calendar.vue"
import {
apiGetAttendanceCalendar,
apiGetAttendanceRule
} from "@/common/http.api.js"
export default {
components: {
RenCalendar
},
data() {
return {
attendanceRule: [],
records: [],
curDate: '',
curMonth: '',
markDays: [], //标记为红色的点
markBlueDays: [], //标记为蓝色的点
attendanceCalendar: [],
checkinCount: 0, //打卡次数
currentAttendance: null
}
},
async onReady() {
let today = this.$refs.ren.getToday().date;
this.curDate = today;
this.curMonth = today.substring(0, 7)
//获取标记点
this.getMarkPoints();
this.getAttendanceRule();
},
methods: {
async getAttendanceRule() {
this.attendanceRule = await apiGetAttendanceRule()
},
async getMarkPoints() {
this.markDays = []
this.markBlueDays = []
this.attendanceCalendar = await apiGetAttendanceCalendar(this.curMonth)
if (this.attendanceCalendar && this.attendanceCalendar.missList?.length > 0) {
this.markDays = [...this.attendanceCalendar.missList];
}
if (this.attendanceCalendar && this.attendanceCalendar.noMissList?.length > 0) {
this.markBlueDays = [...this.attendanceCalendar.noMissList];
}
this.getAttendanceData();
},
onDayClick(data) {
this.curDate = data.date;
this.getAttendanceData()
},
onMonthClick(data) {
this.curMonth = data;
this.getMarkPoints();
},
//获取打卡日期数据
getAttendanceData() {
this.records = []
this.checkinCount = 0;
let data;
if (this.attendanceCalendar && this.attendanceCalendar.attendanceList?.length > 0) {
data = this.attendanceCalendar.attendanceList.filter(val => val.date == this.curDate)[0]
this.currentAttendance = data
}
if (data?.start) {
this.checkinCount++;
this.records.push(data.start)
} else {
this.records.push({
workType: 1,
note: "未打卡",
attendanceTime: '',
attendanceType: '',
attendanceAddress: '',
isStartMiss: data?.isStartMiss
})
}
if (data?.end) {
this.checkinCount++;
this.records.push(data.end)
} else {
this.records.push({
workType: 2,
note: "未打卡",
attendanceTime: '',
attendanceType: '',
attendanceAddress: '',
isEndMiss: data?.isEndMiss
})
}
}
}
}
</script>
<style scoped>
.calendar {
height: 100vh;
background-color: #FFF;
}
.attendance-container {
padding: 32rpx 40rpx;
border-radius: 24rpx;
}
.shift-info {
margin-bottom: 40rpx;
color: #666;
font-size: 30rpx;
line-height: 50rpx;
}
.bold {
font-weight: bold;
color: #222;
}
.timeline {
border-left: 4rpx solid #e0e0e0;
margin-left: 16rpx;
padding-left: 36rpx;
}
.timeline-item {
position: relative;
margin-bottom: 54rpx;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.dot {
position: absolute;
left: -47rpx;
top: 16rpx;
width: 20rpx;
height: 20rpx;
background: #AEAEAE;
border: 4rpx solid #b3b3b3;
border-radius: 50%;
}
.content {
margin-left: 0;
}
.time-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.title {
font-size: 32rpx;
margin-right: 12rpx;
}
.time {
font-size: 32rpx;
color: #222;
margin-right: 12rpx;
}
.tag {
background: #e6f7ff;
color: #1890ff;
border-radius: 8rpx;
padding: 4rpx 16rpx;
font-size: 24rpx;
margin-left: 8rpx;
}
.location {
color: #666;
font-size: 28rpx;
margin-bottom: 4rpx;
display: flex;
align-items: center;
}
.icon {
margin-right: 8rpx;
}
.type {
color: #888;
font-size: 26rpx;
}
.pos-image {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
</style>
后端接口代码:
package com.ynfy.buss.attendance.attendance.service.impl;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ynfy.buss.attendance.attendance.entity.Attendance;
import com.ynfy.buss.attendance.attendance.entity.dto.AttendanceDTO;
import com.ynfy.buss.attendance.attendance.entity.vo.AttendanceCalendarVO;
import com.ynfy.buss.attendance.attendance.entity.vo.AttendanceVO;
import com.ynfy.buss.attendance.attendance.entity.vo.TodayAttendanceVO;
import com.ynfy.buss.attendance.attendance.enums.AttendanceType;
import com.ynfy.buss.attendance.attendance.enums.InoutsideType;
import com.ynfy.buss.attendance.attendance.enums.WorkType;
import com.ynfy.buss.attendance.attendance.mapper.AttendanceMapper;
import com.ynfy.buss.attendance.attendance.service.IAttendanceService;
import com.ynfy.buss.attendance.attendancerule.entity.AttendanceRule;
import com.ynfy.buss.attendance.attendancerule.service.IAttendanceRuleService;
import com.ynfy.buss.attendance.calendarholiday.entity.CalendarHoliday;
import com.ynfy.buss.attendance.calendarholiday.service.ICalendarHolidayService;
import com.ynfy.common.utils.GPSUtil;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @Description: 考勤打卡
* @Author: jeecg-boot
* @Date: 2025-07-16
* @Version: V1.0
*/
@Service
public class AttendanceServiceImpl extends ServiceImpl<AttendanceMapper, Attendance> implements IAttendanceService {
@Autowired
private IAttendanceRuleService attendanceRuleService;
@Autowired
private AttendanceMapper attendanceMapper;
@Autowired
private ICalendarHolidayService calendarHolidayService;
/**
* 保存
*
* @return
*/
@Override
public synchronized void saveAttendance(AttendanceDTO dto, String userId) {
if (Objects.isNull(dto.getInoutsideType()) || Objects.isNull(dto.getAttendanceType()) || Objects.isNull(dto.getWorkType())) {
throw new JeecgBootException("参数异常");
}
//获取考勤规则
AttendanceRule attendanceRule = attendanceRuleService.getAttendanceRule();
if (Objects.isNull(attendanceRule)) {
throw new JeecgBootException("未找到考勤规则");
}
if (Objects.isNull(attendanceRule.getAllowOutside()) || !attendanceRule.getAllowOutside()) {
throw new JeecgBootException("管理员未开启外勤打卡权限,请联系管理员。");
}
//获取当天打卡数据
List<Attendance> attendanceList = listAttendanceRecord(DateUtil.formatDate(new Date()), userId, dto.getWorkType());
if (!CollectionUtils.isEmpty(attendanceList)) {
if (CommonConstant.ATTENDANCE_WORK_START.equals(dto.getWorkType())) {
throw new JeecgBootException("上班打卡以最早打卡为准");
}
attendanceList.forEach(val -> {
BeanUtils.copyProperties(dto, val);
val.setAttendanceTime(new Date());
updateById(val);
});
} else {
Attendance attendance = new Attendance();
BeanUtils.copyProperties(dto, attendance);
attendance.setUserId(userId);
attendance.setAttendanceTime(new Date());
save(attendance);
}
}
@Override
public AttendanceVO getAttendanceType(double lng, double lat, String userId) {
AttendanceVO attendanceVO = new AttendanceVO();
//获取考勤规则
AttendanceRule attendanceRule = attendanceRuleService.getAttendanceRule();
if (isLegwork(attendanceRule, lng, lat)) {
attendanceVO.setInoutsideType(InoutsideType.toJson(InoutsideType.OUTSIDE));
} else {
attendanceVO.setInoutsideType(InoutsideType.toJson(InoutsideType.NORMAL));
}
if (StringUtils.isNotBlank(userId)) {
generateAttendanceType(attendanceRule, userId, attendanceVO);
}
return attendanceVO;
}
/**
* 获取最大迟到打卡时间
*
* @param attendanceRule
* @return
*/
public Date getMaxLateTime(AttendanceRule attendanceRule) {
//开始打卡时间
String startTime = DateUtil.formatDate(new Date()) + " " + attendanceRule.getStartWorkTime() + ":00";
//迟到打卡最大时间
attendanceRule.setAllowLateTime(!Objects.isNull(attendanceRule.getAllowLateTime()) ? attendanceRule.getAllowLateTime() : 0);
return DateUtil.offsetMinute(DateUtil.parseDateTime(startTime), attendanceRule.getAllowLateTime());
}
public void generateAttendanceType(AttendanceRule attendanceRule, String userId, AttendanceVO attendanceVO) {
AttendanceType attendanceType = null;
Date maxLateTime = getMaxLateTime(attendanceRule);
if (calendarHolidayService.isWorkDay(attendanceRule, DateUtil.formatDate(new Date()))) { //如果是工作日
//获取当天打卡数据
List<Attendance> attendanceList = listAttendanceRecord(DateUtil.formatDate(new Date()), userId, null);
//没有签到数据,并且没有超过最大允许迟到时间,则为上班打卡
if (CollectionUtils.isEmpty(attendanceList) && DateUtil.compare(maxLateTime, new Date()) >= 0) {
if (isLatework(attendanceRule)) {//迟到打卡
attendanceType = AttendanceType.LATE;
} else { //正常打卡
attendanceType = AttendanceType.NORMAL;
}
attendanceVO.setWorkType(WorkType.toJson(WorkType.START_WORD));
} else {//下班打卡
if (isLeaveEarly()) {//早退打卡
attendanceType = AttendanceType.EARLY;
} else {
attendanceType = AttendanceType.NORMAL;
}
attendanceVO.setWorkType(WorkType.toJson(WorkType.END_WORD));
}
attendanceVO.setAttendanceType(AttendanceType.toJson(attendanceType));
attendanceVO.setIsWorkDay(true);
} else { //不是工作日
attendanceVO.setAttendanceType(AttendanceType.toJson(AttendanceType.NORMAL));
List<Attendance> attendanceList = listAttendanceRecord(DateUtil.formatDate(new Date()), userId, null);
if (!CollectionUtils.isEmpty(attendanceList)) {
Attendance start = attendanceList.stream().filter(attendance -> CommonConstant.ATTENDANCE_WORK_START.equals(attendance.getWorkType())).findFirst().orElse(null);
if (!Objects.isNull(start)) { //已经打过上班卡
attendanceVO.setWorkType(WorkType.toJson(WorkType.END_WORD));
} else {
attendanceVO.setWorkType(WorkType.toJson(WorkType.START_WORD));
}
} else {
attendanceVO.setWorkType(WorkType.toJson(WorkType.START_WORD));
}
attendanceVO.setIsWorkDay(false);
}
}
/**
* 是否外勤打卡
*
* @return
*/
public boolean isLegwork(AttendanceRule attendanceRule, double lng, double lat) {
if (GPSUtil.getDistance(Double.parseDouble(attendanceRule.getAttendanceLongitude()), Double.parseDouble(attendanceRule.getAttendanceLatitude()), lng, lat) > Double.parseDouble(attendanceRule.getAllowCheckinRange())) {
return true;
}
return false;
}
/**
* 是否迟到打卡
*
* @return
*/
public boolean isLatework(AttendanceRule attendanceRule) {
String startTime = DateUtil.formatDate(new Date()) + " " + attendanceRule.getStartWorkTime() + ":00";
return DateUtil.compare(new Date(), DateUtil.parseDateTime(startTime)) > 0;
}
/**
* 是否早退打卡
*
* @return
*/
@Override
public Boolean isLeaveEarly() {
AttendanceRule attendanceRule = attendanceRuleService.getAttendanceRule();
if (calendarHolidayService.isWorkDay(attendanceRule, DateUtil.formatDate(new Date()))) {
if (!Objects.isNull(attendanceRule) && StringUtils.isNotEmpty(attendanceRule.getEndWorkTime())) {
String endTime = DateUtil.formatDate(new Date()) + " " + attendanceRule.getEndWorkTime() + ":00";
return DateUtil.compare(DateUtil.parseDateTime(endTime), new Date()) > 0;
}
}
return false;
}
/**
* 查询打卡记录
*
* @param date
* @param userId
* @param workType
* @return
*/
@Override
public List<Attendance> listAttendanceRecord(String date, String userId, Integer workType) {
return attendanceMapper.listAttendanceRecord(date, userId, workType);
}
@Override
public TodayAttendanceVO listTodayAttendance(String userId) {
TodayAttendanceVO todayAttendance = new TodayAttendanceVO();
AttendanceRule attendanceRule = attendanceRuleService.getAttendanceRule();
Date maxLateTime = getMaxLateTime(attendanceRule);
List<Attendance> attendanceList = listAttendanceRecord(DateUtil.formatDate(new Date()), userId, null);
if (!calendarHolidayService.isWorkDay(attendanceRule, DateUtil.formatDate(new Date()))) { //如果不是工作日,无需打卡
todayAttendance.setIsNeedAttendance(false);
} else {
todayAttendance.setIsNeedAttendance(true);
}
if (!CollectionUtils.isEmpty(attendanceList)) {
Attendance start = attendanceList.stream().filter(attendance -> CommonConstant.ATTENDANCE_WORK_START.equals(attendance.getWorkType())).findFirst().orElse(null);
Attendance end = attendanceList.stream().filter(attendance -> CommonConstant.ATTENDANCE_WORK_END.equals(attendance.getWorkType())).findFirst().orElse(null);
if (!Objects.isNull(start)) {
todayAttendance.setStart(start);
todayAttendance.setIsStartMiss(false);
} else if (DateUtil.compare(new Date(), maxLateTime) > 0) {
todayAttendance.setIsStartMiss(todayAttendance.getIsNeedAttendance() ? true : null);
}
if (!Objects.isNull(end)) {
todayAttendance.setEnd(end);
todayAttendance.setIsEndMiss(false);
}
} else if (DateUtil.compare(new Date(), maxLateTime) > 0) {
todayAttendance.setIsStartMiss(todayAttendance.getIsNeedAttendance() ? true : null);
}
return todayAttendance;
}
@Override
public AttendanceCalendarVO getAttendanceCalendar(String date, String userId) {
AttendanceCalendarVO attendanceCalendar = new AttendanceCalendarVO();
List<CalendarHoliday> calendarHolidayList = calendarHolidayService.getAttendanceCalendar(date);
//按月查询考勤记录
List<Attendance> attendanceList = listMonthAttendance(date, userId);
List<TodayAttendanceVO> todayAttendanceList = new ArrayList<>();
if (!CollectionUtils.isEmpty(calendarHolidayList)) {
AttendanceRule attendanceRule = attendanceRuleService.getAttendanceRule();
calendarHolidayList.forEach(calendarHoliday -> {
if (DateUtil.compare(DateUtil.parseDate(DateUtil.formatDate(new Date())), calendarHoliday.getCurrentDay()) >= 0) {
if (calendarHolidayService.isWorkDay(attendanceRule, DateUtil.formatDate(calendarHoliday.getCurrentDay()))) {
TodayAttendanceVO todayAttendance = new TodayAttendanceVO();
todayAttendance.setDate(DateUtil.formatDate(calendarHoliday.getCurrentDay()));
if (!CollectionUtils.isEmpty(attendanceList)) {
Attendance start = attendanceList.stream().filter(attendance -> DateUtil.formatDate(calendarHoliday.getCurrentDay()).equals(DateUtil.formatDate(attendance.getAttendanceTime())) && CommonConstant.ATTENDANCE_WORK_START.equals(attendance.getWorkType())).findFirst().orElse(null);
Attendance end = attendanceList.stream().filter(attendance -> DateUtil.formatDate(calendarHoliday.getCurrentDay()).equals(DateUtil.formatDate(attendance.getAttendanceTime())) && CommonConstant.ATTENDANCE_WORK_END.equals(attendance.getWorkType())).findFirst().orElse(null);
if (!Objects.isNull(start)) {
todayAttendance.setStart(start);
todayAttendance.setIsStartMiss(false);
} else {
todayAttendance.setIsStartMiss(true);
}
if (!Objects.isNull(end)) {
todayAttendance.setEnd(end);
todayAttendance.setIsEndMiss(false);
} else {
if (DateUtil.compare(DateUtil.parseDate(DateUtil.formatDate(new Date())), calendarHoliday.getCurrentDay()) > 0) {
todayAttendance.setIsEndMiss(true);
}
}
} else {
todayAttendance.setIsStartMiss(true);
if (DateUtil.compare(DateUtil.parseDate(DateUtil.formatDate(new Date())), calendarHoliday.getCurrentDay()) > 0) {
todayAttendance.setIsEndMiss(true);
}
}
if (todayAttendance.getIsStartMiss() || (!Objects.isNull(todayAttendance.getIsEndMiss()) && todayAttendance.getIsEndMiss())) {
todayAttendance.setHasMiss(true);
} else {
todayAttendance.setHasMiss(false);
}
todayAttendance.setIsWorkDay(true);
todayAttendanceList.add(todayAttendance);
} else { //休息日打卡记录
List<Attendance> tmpList = attendanceList.stream().filter(attendance -> DateUtil.formatDate(calendarHoliday.getCurrentDay()).equals(DateUtil.formatDate(attendance.getAttendanceTime()))).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(tmpList)) {
TodayAttendanceVO todayAttendance = new TodayAttendanceVO();
todayAttendance.setDate(DateUtil.formatDate(calendarHoliday.getCurrentDay()));
Attendance start = tmpList.stream().filter(attendance -> CommonConstant.ATTENDANCE_WORK_START.equals(attendance.getWorkType())).findFirst().orElse(null);
if (!Objects.isNull(start)) {
todayAttendance.setStart(start);
}
Attendance end = tmpList.stream().filter(attendance -> CommonConstant.ATTENDANCE_WORK_END.equals(attendance.getWorkType())).findFirst().orElse(null);
if (!Objects.isNull(end)) {
todayAttendance.setEnd(end);
}
todayAttendance.setHasMiss(false);
todayAttendance.setIsWorkDay(false);
todayAttendanceList.add(todayAttendance);
}
}
}
});
}
attendanceCalendar.setAttendanceList(todayAttendanceList);
attendanceCalendar.setMissList(todayAttendanceList.stream().filter(TodayAttendanceVO::getHasMiss).map(TodayAttendanceVO::getDate).collect(Collectors.toList()));
attendanceCalendar.setNoMissList(todayAttendanceList.stream().filter(todayAttendance -> !todayAttendance.getHasMiss()).map(TodayAttendanceVO::getDate).collect(Collectors.toList()));
return attendanceCalendar;
}
@Override
public List<Attendance> listMonthAttendance(String date, String userId) {
return attendanceMapper.listMonthAttendance(date, userId);
}
}
每年一月一日定时任务生成节假日日历数据。
package com.ynfy.buss.attendance.calendarholiday.service.impl;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ynfy.buss.attendance.attendancerule.entity.AttendanceRule;
import com.ynfy.buss.attendance.calendarholiday.entity.CalendarHoliday;
import com.ynfy.buss.attendance.calendarholiday.mapper.CalendarHolidayMapper;
import com.ynfy.buss.attendance.calendarholiday.service.ICalendarHolidayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* @Description: 节假日日历
* @Author: jeecg-boot
* @Date: 2025-07-18
* @Version: V1.0
*/
@Service
public class CalendarHolidayServiceImpl extends ServiceImpl<CalendarHolidayMapper, CalendarHoliday> implements ICalendarHolidayService {
@Autowired
private CalendarHolidayMapper calendarHolidayMapper;
@Value("${calendar.holiday.api}")
private String calendarHolidayApi;
@Value("${calendar.holiday.key}")
private String key;
/**
* 生成节假日数据
*
* @param year
* @param type
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void generateHoliday(Integer year, Integer type) {
List<CalendarHoliday> holidayList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
int month = i + 1;
String date;
if (month < 10) {
date = year + "-0" + month;
} else {
date = year + "-" + month;
}
JSONObject resultData = JSON.parseObject(HttpUtil.get(calendarHolidayApi + "?key=" + key + "&date=" + date + "&type=" + type, CharsetUtil.CHARSET_UTF_8));
if (!Objects.isNull(resultData)) {
JSONObject result = resultData.getJSONObject("result");
JSONArray array = result.getJSONArray("list");
if (!Objects.isNull(array) && !array.isEmpty()) {
for (int index = 0; index < array.size(); index++) {
JSONObject jsonObject = array.getJSONObject(index);
CalendarHoliday holiday = new CalendarHoliday();
holiday.setCurrentYear(String.valueOf(year));
holiday.setCurrentDay(jsonObject.getDate("date"));
holiday.setIsWorkDay(!jsonObject.getBoolean("isnotwork"));
holiday.setJsonData(jsonObject.toJSONString());
holidayList.add(holiday);
}
}
}
}
remove(new LambdaQueryWrapper<CalendarHoliday>().eq(CalendarHoliday::getCurrentYear, String.valueOf(year)));
if (!CollectionUtils.isEmpty(holidayList)) {
saveBatch(holidayList);
}
}
/**
* 是否需要上班
*/
@Override
public boolean isWorkDay(AttendanceRule attendanceRule, String date) {
CalendarHoliday calendarHoliday = calendarHolidayMapper.getByDate(date);
if (Objects.isNull(calendarHoliday)) {
return false;
}
Boolean needWork;
List<String> weekList = Arrays.asList(attendanceRule.getAttendanceWeek().split(","));
if (!CollectionUtils.isEmpty(weekList)) {
JSONObject jsonObject = JSONObject.parseObject(calendarHoliday.getJsonData());
String weekday = jsonObject.getString("weekday");
if (weekList.contains(weekday)) {//日期需要打卡
needWork = true;
} else {
needWork = false;
}
if (!Objects.isNull(attendanceRule.getIsAutoStatutoryHoliday()) && attendanceRule.getIsAutoStatutoryHoliday()) { //法定节假日自动排休
needWork = calendarHoliday.getIsWorkDay();
}
} else {
needWork = false;
}
return needWork;
}
@Override
public List<CalendarHoliday> getAttendanceCalendar(String date) {
return calendarHolidayMapper.getAttendanceCalendar(date);
}
}
是否需要上班接口需要获取考勤规则中的打卡星期,如果在打卡星期范围内,则默认为上班日,不在范围内则为休息。如果配置法定节假日自动排休,则法定节假日休息日对应需要将打卡日期改为休息,法定节假日补班日(上班日)对应需要将打卡日期改为上班。
(6)安卓app端打卡配置
开通定位权限,填写高德地图申请的key。
Map地图模块。
![]() |