前言

近期开发一个用golang同步企业微信的打卡数据到HR系统中间表的应用,开发的过程中遇到了一些问题,在这里把源码及遇到的问题及解决方法分亨出来供大家参考一下。


一、了解企业微信的打卡数据接口文档?

企微的钉钉打卡数据接口文档大家可以到企微的开放平台查看

接口文档很简单:

{"opencheckindatatype": 3,"starttime": 1492617600,"endtime": 1492790400,"useridlist": ["james","paul"]
}

1. 获取记录时间跨度不超过30天 2. 用户列表不超过100个。若用户超过100个,请分批获取 3. 有打卡记录即可获取打卡数据,与当前"打卡应用"是否开启无关 4. 标准打卡时间只对于固定排班和自定义排班两种类型有效 5. 接口调用频率限制为600次/分钟

话不多説,直接分亨源码

二、获取企业微信打卡记录源码

1.接口源码

package clockinimport ("bytes""context""encoding/json""errors""fmt""github.com/go-redis/redis/v8""github.com/spf13/viper""go.uber.org/ratelimit""gorm.io/driver/sqlserver""gorm.io/gorm""io/ioutil""log""net/http""net/url""strconv""strings""sync""time""wdmtool/util"
)type QyoaToken struct {Errcode     int    `json:"errcode"`AccessToken string `json:"access_token"`Errmsg      string `json:"errmsg"`ExpiresIn   int64  `json:"expires_in"`ExpiresTime int64  `json:"expires_time"`
}
type ListIdReq struct {Cursor string `json:"cursor"`Limit  uint64 `json:"limit"`
}
type UserIdRec struct {Userid     string `json:"userid"`Department int    `json:"department"`
}
type ListIdResult struct {Errcode    int         `json:"errcode"`Errmsg     string      `json:"errmsg"`NextCursor string      `json:"next_cursor"`DeptUser   []UserIdRec `json:"dept_user"`
}
type CheckinDataReq struct {OpenCheckInDataType int      `json:"opencheckindatatype"`StartTime           int64    `json:"starttime"`EndTime             int64    `json:"endtime"`UserIdList          []string `json:"useridlist"`
}
type CheckinDataResult struct {Errcode     int           `json:"errcode"`Errmsg      string        `json:"errmsg"`CheckinData []CheckinData `json:"checkindata"`
}
type CheckinData struct {Userid         string   `json:"userid"`Groupname      string   `json:"groupname"`CheckinType    string   `json:"checkin_type"`ExceptionType  string   `json:"exception_type"`CheckinTime    int64    `json:"checkin_time"`LocationTitle  string   `json:"location_title"`LocationDetail string   `json:"location_detail"`Wifiname       string   `json:"wifiname"`Wifimac        string   `json:"wifimac"`Mediaids       []string `json:"mediaids"`DeviceId       string   `json:"deviceid"`SchCheckinTime int64    `json:"sch_checkin_time"`Groupid        int      `json:"groupid"`ScheduleId     int      `json:"schedule_id"`TimelineId     int      `json:"timeline_id"`
}type WdmCheckinData struct {CheckType      string `gorm:"column:checkType;type:varchar(50);comment:'考勤类型 OnDuty:上班 OffDuty:下班'"`DDid           string `gorm:"column:DDid;type:varchar(50);comment:'钉钉ID'"`LocationResult string `gorm:"column:locationResult;type:varchar(50);comment:'位置结果:Normal:范围内Outside:范围外NotSigned:未打卡'"`RecordId       string `gorm:"column:recordId;type:varchar(50);comment:'打卡记录ID'"`UserCheckTime  string `gorm:"column:userCheckTime;type:varchar(50);comment:'实际打卡时间'"`UserId         string `gorm:"column:userId;type:varchar(50);comment:'打卡人的UserID'"`WorkDate       string `gorm:"column:workDate;type:varchar(50);comment:'工作日'"`GmtModified    string `gorm:"column:gmtModified;type:varchar(50);comment:'打卡记录修改时间'"`IsLegal        string `gorm:"column:isLegal;type:varchar(50);comment:'Y:合法 N:不合法'"` //是否合法BaseCheckTime  string `gorm:"column:baseCheckTime;type:varchar(50);comment:'计算迟到和早退,基准时间;也可作为排班打卡时间'"`UserAddress    string `gorm:"column:userAddress;type:varchar(2000);comment:'用户打卡地址 如果是考勤机打卡 userAddress 返回的是考勤机名称'"`TimeResult     string `gorm:"column:timeResult;type:varchar(50);comment:'打卡结果:Normal:正常Early:早退Late:迟到SeriousLate严重迟到Absenteeism:旷工迟到 NotSigned:未打卡'"`DeviceId       string `gorm:"column:deviceId;type:varchar(50);comment:'deviceId'"` //打卡设备ID//ATM:考勤机打卡(指纹/人脸打卡)//BEACON:IBeacon//DING_ATM:钉钉考勤机(考勤机蓝牙打卡)//USER:用户打卡//BOSS:老板改签//APPROVE:审批系统//SYSTEM:考勤系统//AUTO_CHECK:自动打卡SourceType     string `gorm:"column:sourceType;type:varchar(50);comment:'数据来源'"`PlanCheckTime  string `gorm:"column:planCheckTime;type:varchar(50);comment:'排班打卡时间'"`GmtCreate      string `gorm:"column:gmtCreate;type:varchar(50);comment:'打卡记录创建时间'"`LocationMethod string `gorm:"column:locationMethod;type:varchar(50);comment:'MAP'"` //定位方法PlanId         string `gorm:"column:planId;type:varchar(50);comment:'排班ID'"`GroupId        string `gorm:"column:groupId;type:varchar(50);comment:'考勤组ID'"`InsertTime     string `gorm:"column:InsertTime;type:datetime;comment:'InsertTime'"`
}var ctx = context.Background()
var DB *gorm.DB
var rdb *redis.Clientfunc Sync() {InitHrDB()tokenTxl, err := GetToken(viper.GetString("QyoaWork.WdmTXLSecret"))if err != nil {log.Println("获取企业微信接口token返回错误", err.Error())return}log.Println("获取企业微信通讯录接口token:", tokenTxl)allUserIds, err := getUserIdDepId(tokenTxl)//log.Println(allUserIds)if err != nil {log.Println("获取企业微信人员返回错误", err.Error())return}if len(allUserIds) == 0 {log.Println("获取企业微信人员返回人数为空无需处理")return}tokenKaoqin, err := GetToken(viper.GetString("QyoaWork.WdmAttendanceSecret"))if err != nil {log.Println("获取企业微信接口获取考勤token返回错误", err.Error())return}if util.Fday > 0 {util.Fday = -1}log.Println("获取企业微信考蓝带接口token:", tokenKaoqin)timeObj := time.Now()var stime time.Timevar etime time.TimehasTime := 0if util.Fday < 0 {// 获取开始日期oldTime := timeObj.AddDate(0, 0, util.Fday)oldDate := oldTime.Format("2006-01-02")log.Println(oldDate)stime, _ = time.ParseInLocation("2006-01-02 15:04:05", oldDate+" 00:00:00", time.Local)// 获取结束日期newTime := timeObj.AddDate(0, 0, -1)endDate := newTime.Format("2006-01-02")log.Println(endDate)etime, _ = time.ParseInLocation("2006-01-02 15:04:05", endDate+" 23:59:59", time.Local)etime.Add(time.Second * time.Duration(1))hasTime = 1}if util.Fsday != "" && util.Feday != "" {stime, _ = time.ParseInLocation("2006-01-02 15:04:05", util.Fsday+" 00:00:00", time.Local)etime, _ = time.ParseInLocation("2006-01-02 15:04:05", util.Feday+" 23:59:59", time.Local)etime.Add(time.Second * time.Duration(1))hasTime = 1}if hasTime == 0 {log.Println("打卡开始日期和结束日期错误")return}wg := sync.WaitGroup{}workers := 5in := make(chan struct{}, workers)rl := ratelimit.New(10)defer close(in)i := 0for _, User := range allUserIds {in <- struct{}{}wg.Add(1)rl.Take()go Getcheckindata(User, tokenKaoqin, stime.Unix(), etime.Unix(), &wg, in)i++}wg.Wait()fmt.Println("sync")
}
func Getcheckindata(user UserIdRec, token string, Stime int64, Etime int64, wg *sync.WaitGroup, out chan struct{}) error {defer func() {wg.Done()<-out}()log.Println("Getcheckindata")//获取打卡记录var Url string = "https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckindata?access_token=" + tokenlog.Println("获取企业微信人员打卡数据URL:", Url)var param CheckinDataReqparam.OpenCheckInDataType = 3param.StartTime = Stimeparam.EndTime = Etimeparam.UserIdList = []string{user.Userid}paramStr, _ := json.Marshal(param)log.Println("获取企业微信人员打卡数据请求参数:", string(paramStr))req, err := http.NewRequest("POST", Url, bytes.NewBuffer(paramStr))if err != nil {log.Println(err.Error())return err}log.Println("11111")client := &http.Client{}resp, err := client.Do(req)defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)if err != nil {log.Println(err.Error())return err}log.Println(string(body))log.Println("222222")var Result CheckinDataResulterr = json.Unmarshal(body, &Result)log.Println(err)if err != nil {return err}log.Println("获取企业微信人员打卡结果:", Result)log.Println("333333333")if Result.Errcode == 0 && Result.Errmsg == "ok" {var wdmrec WdmCheckinDatacheckTimeStr := ""schCheckTimeStr := ""locationResult := ""timeResult := ""for _, rec := range Result.CheckinData {log.Println(rec)switch rec.CheckinType {case "上班打卡":wdmrec.CheckType = "OnDuty"case "下班打卡":wdmrec.CheckType = "OffDuty"default:wdmrec.CheckType = rec.CheckinType}//时间异常,地点异常,未打卡,wifi异常,非常用设备如果有多个异常,以分号间隔locationResult = "Normal"timeResult = "Normal"if rec.ExceptionType != "" {if strings.Contains(rec.ExceptionType, "地点异常") {locationResult = "Outside"}if strings.Contains(rec.ExceptionType, "时间异常") {timeResult = ""}if strings.Contains(rec.ExceptionType, "未打卡") {timeResult = "NotSigned"}if strings.Contains(rec.ExceptionType, "wifi异常") {timeResult = "Outside"}if strings.Contains(rec.ExceptionType, "非常用设备") {locationResult = rec.ExceptionType}}checkTimeStr = time.Unix(rec.CheckinTime, 0).Format("2006/01/02 15:04:05")if rec.SchCheckinTime > 0 {schCheckTimeStr = time.Unix(rec.SchCheckinTime, 0).Format("2006/01/02") + " 00:00:00"}wdmrec.DDid = rec.Useridwdmrec.LocationResult = locationResultwdmrec.RecordId = ""wdmrec.UserCheckTime = checkTimeStrwdmrec.UserId = rec.Useridwdmrec.WorkDate = schCheckTimeStrwdmrec.GmtModified = checkTimeStrif locationResult == "Normal" && timeResult == "Normal" {wdmrec.IsLegal = "Y"} else {wdmrec.IsLegal = "N"}wdmrec.BaseCheckTime = time.Unix(rec.CheckinTime, 0).Format("2006/01/02 15:04") + ":00"wdmrec.UserAddress = rec.LocationTitlewdmrec.TimeResult = timeResultwdmrec.DeviceId = rec.DeviceIdwdmrec.SourceType = "USER"wdmrec.PlanCheckTime = checkTimeStrwdmrec.GmtCreate = checkTimeStrwdmrec.LocationMethod = "MAP"wdmrec.PlanId = strconv.Itoa(rec.ScheduleId)wdmrec.GroupId = strconv.Itoa(rec.Groupid)wdmrec.InsertTime = time.Now().Format("2006-01-02 15:04:05.999")DB.Table("LsyncQYWXforK_Cardwdm_back").Debug().Create(&wdmrec)}} else {return errors.New(strconv.Itoa(Result.Errcode) + Result.Errmsg)}return nil}
func getUserIdDepId(token string) (UserList []UserIdRec, err error) {var Url string = "https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=" + token//log.Println("获取企业微信人员打卡数据URL:", Url)var param ListIdReq//param["access_token"] = []string{token}param.Limit = 2000client := &http.Client{}for {paramStr, _ := json.Marshal(param)req, err := http.NewRequest("POST", Url, bytes.NewBuffer(paramStr))if err != nil {log.Println(err.Error())return UserList, err}resp, err := client.Do(req)body, err := ioutil.ReadAll(resp.Body)if err != nil {log.Println(err.Error())resp.Body.Close()return UserList, err}var Result ListIdResulterr = json.Unmarshal(body, &Result)if err != nil {return UserList, err}if Result.Errcode == 0 && Result.Errmsg == "ok" {UserList = append(UserList, Result.DeptUser...)} else {resp.Body.Close()return UserList, errors.New(strconv.Itoa(Result.Errcode) + Result.Errmsg)}if len(Result.DeptUser) == 0 || Result.NextCursor == "" {break}param.Cursor = Result.NextCursorresp.Body.Close()}return UserList, nil}
func GetToken(secret string) (token string, err error) {corpid := viper.GetString("QyoaWork.CorpID")appSecrect := secrettokenredishost := viper.GetString("QyoaWork.TokenRedisHost")tokenredisport := viper.GetString("QyoaWork.TokenRedisPort")rdb := redis.NewClient(&redis.Options{Addr:     tokenredishost + ":" + tokenredisport,Password: "", // no password setDB:       0,  // use default DB})defer func() {rdb.Close()}()key := "qyoa_access_token_" + corpid + "_" + appSecrectvar Mytoken QyoaTokenlog.Println(key)strToken, err := rdb.Get(ctx, key).Result()log.Println(strToken)if err == nil {err = json.Unmarshal([]byte(strToken), &Mytoken)if err != nil {return "", err}if Mytoken.Errcode == 0 && Mytoken.ExpiresTime > time.Now().Unix() {log.Println("从redis里获取了token")return Mytoken.AccessToken, nil}}url := "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + appSecrectresp, err := http.Get(url)log.Println("gettoken resp", resp)if err != nil {return "", err}defer resp.Body.Close()b, err := ioutil.ReadAll(resp.Body)if err != nil {return "", err}err = json.Unmarshal(b, &Mytoken)if err != nil {return "", err}if Mytoken.Errcode == 0 && Mytoken.Errmsg == "ok" {rdb.SetEX(ctx, key, string(b), time.Duration(Mytoken.ExpiresIn-300)*time.Second)return Mytoken.AccessToken, nil}return "", errors.New(strconv.Itoa(Mytoken.Errcode) + Mytoken.Errmsg)}func InitHrDB() *gorm.DB {driver := viper.GetString("HrDatabase.Driver")host := viper.GetString("HrDatabase.Host")port := viper.GetString("HrDatabase.Port")database := viper.GetString("HrDatabase.Database")username := viper.GetString("HrDatabase.Username")password := viper.GetString("HrDatabase.Password")query := url.Values{}query.Add("database", database)query.Add("encrypt", "disable")dsn := &url.URL{Scheme:   driver,User:     url.UserPassword(username, password),Host:     host + ":" + port,RawQuery: query.Encode(),}fmt.Println(dsn.String())db, err := gorm.Open(sqlserver.Open(dsn.String()), &gorm.Config{})if err != nil {panic("failed to connect database, err" + err.Error())}DB = dbreturn db
}

三、源码解析
源码中涉及到获取接口token的部分,获取到token后存放到redis

func GetToken(secret string) (token string, err error) {corpid := viper.GetString("QyoaWork.CorpID")appSecrect := secrettokenredishost := viper.GetString("QyoaWork.TokenRedisHost")tokenredisport := viper.GetString("QyoaWork.TokenRedisPort")rdb := redis.NewClient(&redis.Options{Addr:     tokenredishost + ":" + tokenredisport,Password: "", // no password setDB:       0,  // use default DB})defer func() {rdb.Close()}()key := "qyoa_access_token_" + corpid + "_" + appSecrectvar Mytoken QyoaTokenlog.Println(key)strToken, err := rdb.Get(ctx, key).Result()log.Println(strToken)if err == nil {err = json.Unmarshal([]byte(strToken), &Mytoken)if err != nil {return "", err}if Mytoken.Errcode == 0 && Mytoken.ExpiresTime > time.Now().Unix() {log.Println("从redis里获取了token")return Mytoken.AccessToken, nil}}url := "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + appSecrectresp, err := http.Get(url)log.Println("gettoken resp", resp)if err != nil {return "", err}defer resp.Body.Close()b, err := ioutil.ReadAll(resp.Body)if err != nil {return "", err}err = json.Unmarshal(b, &Mytoken)if err != nil {return "", err}if Mytoken.Errcode == 0 && Mytoken.Errmsg == "ok" {rdb.SetEX(ctx, key, string(b), time.Duration(Mytoken.ExpiresIn-300)*time.Second)return Mytoken.AccessToken, nil}return "", errors.New(strconv.Itoa(Mytoken.Errcode) + Mytoken.Errmsg)}

获取token里可能汲到应用接口的token及企微通讯录的token,两种token是不一样的,所以封装成了一个函数

另外企微对获取打卡数据限频了,我们用ratelimit做了请求接口的限频的处理:

wg := sync.WaitGroup{}workers := 5in := make(chan struct{}, workers)rl := ratelimit.New(10)defer close(in)i := 0for _, User := range allUserIds {in <- struct{}{}wg.Add(1)rl.Take()go Getcheckindata(User, tokenKaoqin, stime.Unix(), etime.Unix(), &wg, in)i++}wg.Wait()fmt.Println("sync")

同时在入库时,遇到了

[SQL Server]如果 DML 语句包含不带 INTO 子句的 OUTPUT 子句,则该语句的目标表 'LsyncQYWXforK_Cardwdm_back' 不能具有任何启用的触发器。

这个错误主要是golang在入库里生成的sql语句会加上

OUTPUT INSERTED."id"

mssqlserver里在执行时就报错了,解决法办是在定义WdmCheckinData这个对象时不定义主键,就不会自动生成

总结

这两天太忙了,讲得不是很详细,有问题的大家可以留言沟通。

用golang从企业微信里获取打卡记录的方法相关推荐

  1. 怎么看服务器连接记录_企业微信怎么查看打卡记录?自己和他人的都可以看

      大家在上班的时候,每天要做的第一件事应该就是[打卡].打卡的形式有很多种,最常见的形式刷卡和指纹打卡,这两种传统的打卡方式,都是需要在公司门口安装一个刷卡机器,通过大家打卡的时间点来记录考勤.随着 ...

  2. 公司内服务器微信报警怎么做,一种用企业微信实现信息报警的装置制造方法及图纸...

    [技术实现步骤摘要] 一种用企业微信实现信息报警的装置 本专利技术涉及远程报警 ,特别是涉及一种用企业微信实现信息报警的装置. 技术介绍 目前市场上的各种报警器,有一种是用电话或手机短信实现报警,这种 ...

  3. 这些车企在企业微信里,装上高速的“组织引擎”

    "这真是一场惊险之旅." 今年7月,胡先生一家疾驶在若羌县罗布泊镇国道上,迎面突然冲出一辆大型货车--为了避让,胡太太驾驶的极氪001撞上了路边的石墩,两个轮胎直接报废. 在人迹罕 ...

  4. 企业微信授权获取access_token

    @Slf4j public class QiYeWeChatUtil {/*** 缓存企业微信accessToken*/public static final String QY_WX_TOKEN_k ...

  5. 前端企业微信开发内嵌H5记录

    前端企业微信开发内嵌H5记录(Vue) 文章目录 前端企业微信开发内嵌H5记录(Vue) 一.引入相应JS-SDK 1.JS-SDK 二.授权(网页授权) 1.构造网页授权链接 2.发起授权 3.注入 ...

  6. mysql查询数据库第一条记录_SQL获取第一条记录的方法(sqlserver、oracle、mysql数据库)...

    Sqlserver 获取每组中的第一条记录 在日常生活方面,我们经常需要记录一些操作,类似于日志的操作,最后的记录才是有效数据,而且可能它们属于不同的方面.功能下面,从数据库的术语来说,就是查找出每组 ...

  7. Android获取手机通话记录的方法

    获取手机通话记录流程: 1. 获取ContentResolver; ContentResolver resolver = getContentResolver(); 2.resolver.query( ...

  8. golang对接企业微信群机器人-在线客服系统新消息提醒方式之一【唯一客服】

    最近客服系统对接了一下企业微信的机器人 企业成员(内部)群机器人 只能在企业微信内部群里添加,设置好机器人头像名称之后会得到一个webhook,创建者可使用此wenhook去调用相关api向群里推送消 ...

  9. pywinauto WXWork(企业微信) 获取工作台中的信息

    需求1:家里有一个小店,需要订烟,中秋节前夕订烟日期调整,业务经理提前3天通知了,但是忘记了,然后就错过了,所以我要获取订烟时间,然后通过腾讯的sms,发送到几部手机上 需求2:某丝丽企业微信群需要每 ...

  10. 【企业微信】获取token 发送应用消息

    企业微信获取token 存入redis 设置时长2小时 && 发送企业应用消息接口 1.常量类 package com.ruoyi.common.constant;/*** 微信常用常 ...

最新文章

  1. 迪杰斯特拉算法(C语言实现)
  2. Webpack模块化原理简析
  3. (android)system ui 内存优化
  4. 015_面向对象_异常,包和Object类
  5. idea工作台输出的日志详解_详解linux下nohup日志输出过大问题解决方案--分批切割...
  6. ik分词器实现原理_SpringBoot整合Elasticsearch实现商品搜索
  7. mongodb转实体对像_营销案例丨实体店走出门面冷清的方法:打造体验式门店
  8. win32 destroywindow函数
  9. 红杉中国2021企业数字化年度指南:企业如何制胜数字化浪潮?
  10. java HelloWorld时报错:“找不到或无法加载主类“问题的解决办法
  11. python去重保留唯一一个值_python 去重和保留重复值方法 duplicated 和 drop_duplicates...
  12. PG性能调校(二):数据库硬件及基准评测
  13. r语言把两个折线图图像放到一个图里_OpenCV计算机视觉学习(10)——图像变换(傅里叶变换,高通滤波,低通滤波)...
  14. Python:retrying与tenacity模块失败重跑库
  15. 设置div高度为浏览器可视窗口的高度
  16. 什么是CF , correlation filer ? 【无标题】
  17. c语言课程设计之简易计算器,简易计算器课程设计 帮忙写简单计算器课程设计...
  18. 【物联网+区块链=?】展锐、蚂蚁链、广和通联合发布可信上链模组
  19. html在线添加页码,Wkhtmltopdf添加页码
  20. 几种还款方式解读(包含例子,正在更新)

热门文章

  1. 路由器的网络连接模式(桥接模式和路由模式)
  2. 打开计算机不显示硬盘盘符,电脑硬盘不显示盘符怎么办 移动硬盘不显示盘符的原因...
  3. 真机实战之VLAN隔离,助力网络安全
  4. 北京交通大学计算机学院复试名单2021,北京交通大学2021年硕士研究生复试公告...
  5. python绘制基因结构图_使用biopython可视化染色体和基因元件
  6. linux 搜索s开头的文件,Linux文件查找/内容搜索命令
  7. 浅谈聊天机器人 ChatBot 涉及到的技术点 以及词性标注和关键字提取
  8. mysql杀死锁死的进程_如何杀死MySQL进程
  9. 技术笔试面试题(上)
  10. 蔡勒(Zeller)公式及其推导:快速将任意日期转换为星期数