uniapp+高德地图实现打卡签到、打卡日历

发布于:2025-07-27 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、注册高德地图。

       应用管理创建应用,分别添加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地图模块。

 

 

 


网站公告

今日签到

点亮在社区的每一天
去签到