// @Author cory 2025/3/5 10:27:00 package attendanceMachine import ( "encoding/base64" "encoding/json" "errors" "fmt" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/tiger1103/gfast/v3/api/v1/common/coryCommon" "github.com/tiger1103/gfast/v3/internal/app/system/dao" wxDao "github.com/tiger1103/gfast/v3/internal/app/wxApplet/dao" wxBusAttendance "github.com/tiger1103/gfast/v3/internal/app/wxApplet/logic/busAttendance" wxModel "github.com/tiger1103/gfast/v3/internal/app/wxApplet/model" wxDo "github.com/tiger1103/gfast/v3/internal/app/wxApplet/model/do" "github.com/tiger1103/gfast/v3/library/liberr" tool "github.com/tiger1103/gfast/v3/utility/coryUtils" "golang.org/x/net/context" "io/ioutil" "math" "net/url" "os" "path/filepath" "strconv" "strings" "time" ) func (w AttendanceMachineApi) TaskCreate(ctx context.Context, req *EquipmentTimeClockReq) (res *EquipmentTimeClockRes, err error) { res = new(EquipmentTimeClockRes) err = AttendanceMachine(ctx, req) res.Result = "0" res.Msg = "" r := ghttp.RequestFromCtx(ctx) r.Response.ClearBuffer() r.Response.WriteJson(res) return } func AttendanceMachine(ctx context.Context, req *EquipmentTimeClockReq) (err error) { logs := req.Logs[0] f1 := logs.Location.Longitude f2 := logs.Location.Latitude res, err := wxBusAttendance.ThisInverseGeocodingFunc(f1 + "," + f2) liberr.ErrIsNil(ctx, err) // 上次打卡距离此次打卡不超过3分支,那么提示“你刚刚已打上/下班卡了哦!” currentTime := time.Now() date := tool.New().GetFormattedDate(currentTime) var bair *wxModel.BusAttendanceInfoRes err = wxDao.BusAttendance.Ctx(ctx). Where("openid", logs.UserID). Where("printing_date", date). Fields("clock_on,commuter").OrderDesc("clock_on").Limit().Scan(&bair) liberr.ErrIsNil(ctx, err, "打卡失败!") if err == nil && bair != nil { //判断t1和t2是否相差180秒,如果小于180秒就直接return flag, err := IsWithinThreeMinutes(currentTime, bair.ClockOn) if err != nil { return err } if flag { txt := "" if bair.Commuter == "1" { txt = "您已上班打卡成功!杜绝重复打卡!" } else { txt = "您已下班打卡成功!杜绝重复打卡!" } err = errors.New(txt) return err } } lng, err := strconv.ParseFloat(f1, 64) if err != nil { err = errors.New("精准度转换错误!") return } lat, err := strconv.ParseFloat(f2, 64) if err != nil { err = errors.New("纬度转换错误!") return } lng, lat = coryCommon.GCJ02toWGS84(lng, lat) // 0-0、判断是否开启打卡功能 (禁止打卡和离职都不允许打卡) var userInfo *wxModel.BusConstructionUserInfoRes err = dao.BusConstructionUser.Ctx(ctx).Where("openid", logs.UserID).Scan(&userInfo) if userInfo.TeamId == 0 { err = errors.New("当前用户暂无班组,无法打卡!") return } // 查看当前用户的打卡时间是否请假,如果请假就提示不用打卡 count, err := wxDao.BusAskforleave.Ctx(ctx). Where(wxDao.BusAskforleave.Columns().ProjectId, userInfo.ProjectId). Where(wxDao.BusAskforleave.Columns().Openid, userInfo.Openid). Where("(" + "DATE_FORMAT(" + date + ",'%Y-%m-%d') BETWEEN DATE_FORMAT(start_time,'%Y-%m-%d') AND DATE_FORMAT(end_time,'%Y-%m-%d')" + " or " + "DATE_FORMAT(" + date + ",'%Y-%m-%d') BETWEEN DATE_FORMAT(start_time,'%Y-%m-%d') AND DATE_FORMAT(end_time,'%Y-%m-%d')" + ")").Count() liberr.ErrIsNil(ctx, err) if count > 0 { liberr.ErrIsNil(ctx, errors.New("您已请假,无需打卡!")) return } // 部分班组可以直接跳过在范围内打卡,获取当前用户班组,看看是否是可以在任何地点打卡 value, errvalue := wxDao.SysProjectTeam.Ctx(ctx).Where("id", userInfo.TeamId).Fields("is_clock_in").Value() if errvalue != nil { err = errors.New("获取班组打卡状态失败!") return err } if value.String() == "2" { goto breakHere } // 计算是否在打卡范围中 if true { // 查询当前用户是否有加入班组,有的话看看是什么项目,再根据项目获取到方正,最后根据方正数据得出打卡范围 projectId, _ := wxDao.SysProjectTeamMember.Ctx(ctx).As("a"). LeftJoin("sys_project_team as b on a.team_id = b.id"). Fields("a.project_id"). Where("a.openid", logs.UserID).Value() if projectId != nil { var qf []*wxBusAttendance.ProjectPunchRangeRes err = g.DB().Model("sys_project_punch_range").Where("project_id", projectId).Fields("punch_range").Scan(&qf) if err != nil { err = errors.New("系统错误,请联系管理员!") return err } if len(qf) == 0 { err = errors.New("未设置打卡范围!请联系管理员设置!") return err } countI := len(qf) countII := 0 for _, dakaStr := range qf { countII = countII + 1 var dataInfo coryCommon.DetailedMap err = json.Unmarshal([]byte(fmt.Sprint(dakaStr.PunchRange)), &dataInfo.Positions) flag := coryCommon.RectangularFrameRange(dataInfo, lng, lat) if flag { goto breakHere } else { // 没有一次范围匹配上,那就直接返回 if countII == countI { err = errors.New("不在范围内,打卡无效!") return err } } } } else { err = errors.New("未指定打卡范围,请联系管理员!") return err } } breakHere: if userInfo.Status == "1" { err = errors.New("已离职,请联系管理员!") return } if userInfo.Clock == "2" { err = errors.New("已禁止打卡,请联系管理员!") return } if len(strings.Trim(userInfo.LeaveDate.String(), "")) != 0 { err = errors.New("已退场,请联系管理员!") return } clockStatus := "" // 1、获取当日时间 // 2、查询今日打卡记录 gm := wxDao.BusAttendance.Ctx(ctx). Where(wxDao.BusAttendance.Columns().Openid, logs.UserID). Where(wxDao.BusAttendance.Columns().PrintingDate, date) // 3、如果有判断是否打了上班卡,如果没有就打上班卡 count1, _ := gm.Where("commuter", "1").Count() if count1 == 0 { // 打上班卡 clockStatus = "1" } // 4、如果有判断是否打了下班卡,如果没有就打下班卡 count2, _ := gm.Where("commuter", "2").Count() if clockStatus == "" && count2 == 0 { // 打下班卡 clockStatus = "2" } // 5、上下班卡都打了,那么就提示今日打卡完成 if count1 != 0 && count2 != 0 { // 已完成当日打卡 err = errors.New("当日打卡已完成!") return err } // 提交数据 name := userInfo.NickName if userInfo.UserName != "" { name = userInfo.UserName } // 将base64人脸换成图片存储到本地 imagePath, err := DecodeBase64Image(logs.Photo) if err != nil { return err } rpath := coryCommon.ResourcePublicToFunc(imagePath, 1) attendance := wxDo.BusAttendance{ PacePhoto: rpath, UserName: name, ProjectId: userInfo.ProjectId, Openid: userInfo.Openid, Commuter: clockStatus, Lng: lng, Lat: lat, PrintingDate: date, } // 判断此次打卡状态 默认就为缺勤 dateTime := tool.New().GetFormattedDateTime(time.Now()) if userInfo.ProjectId > 0 { value, err := dao.SysProject.Ctx(ctx).WherePri(userInfo.ProjectId).Fields("punch_range as punchRange").Value() if err != nil { err = errors.New("获取项目打卡范围失败!") } split := strings.Split(value.String(), ",") strType := tool.New().TimeWithin(dateTime, split[0], split[1], clockStatus) attendance.IsPinch = strType attendance.PunchRange = value.String() // 记录项目的打卡时间,保留字段,后期有大用 } // 获取此次打卡的日薪 vl, err := dao.BusConstructionUser.Ctx(ctx).As("a"). LeftJoin("bus_type_of_wage as f on a.type_of_work = f.type_of_work"). Fields("if (a.salary>0, a.salary,f.standard) as salary"). Where("a.openid", logs.UserID).Value() liberr.ErrIsNil(ctx, err, "获取薪资失败") // 记录打卡时间(上下班打卡时间统一用clock_on) attendance.ClockOn = dateTime attendance.DailyWage = vl attendance.Lng = lng attendance.Lat = lat attendance.Location = res.Regeocode.FormattedAddress _, err = wxDao.BusAttendance.Ctx(ctx).Insert(attendance) liberr.ErrIsNil(ctx, err, "添加失败") return } // DecodeBase64Image 函数用于解码 Base64 编码的图片数据并保存为图片 func DecodeBase64Image(encodedStr string) (string, error) { // 进行 URL 解码 decodedURLStr, err := url.QueryUnescape(encodedStr) if err != nil { return "", fmt.Errorf("URL 解码失败: %w", err) } // 提取真正的 Base64 数据 parts := strings.SplitN(decodedURLStr, ",", 2) if len(parts) != 2 { return "", fmt.Errorf("无效的 Base64 编码图片数据格式") } base64Data := parts[1] // 进行 Base64 解码 decodedData, err := base64.StdEncoding.DecodeString(base64Data) if err != nil { return "", fmt.Errorf("Base64 解码失败: %w", err) } ht := coryCommon.Helmet str := coryCommon.Ynr(ht) fn := coryCommon.FileName("face") dir, err := os.Getwd() str = dir + "/" + str + fn + ".jpg" str = filepath.ToSlash(str) // 保存解码后的图片到文件 err = ioutil.WriteFile(str, decodedData, 0644) if err != nil { fmt.Println("保存图片文件出错:", err) return "", fmt.Errorf("保存解码后的图片失败: %w", err) } return str, nil } // 判断两个时间是否相差超过 3 分钟 func isMinutesDifferenceGreaterThanThree(startTime, endTime time.Time) bool { // 计算时间差(单位:秒) diff := endTime.Sub(startTime).Seconds() // 判断是否超过 180 秒(3 分钟) return diff > 180 } // 判断两个时间的秒数差是否 <= 180 秒 func IsWithinThreeMinutes(currentTime time.Time, strTime string) (bool, error) { // 解析字符串时间,使用本地时区 loc, _ := time.LoadLocation("Local") // 获取本地时区 parsedTime, err := time.ParseInLocation("2006-01-02 15:04:05", strTime, loc) if err != nil { fmt.Println("时间解析错误:", err) return false, err } // 计算时间差(秒) diff := math.Abs(currentTime.Sub(parsedTime).Seconds()) // 判断是否相差 180 秒 return diff <= 180, nil }