This commit is contained in:
eryajf 2022-06-02 11:09:47 +08:00
commit 8d28e64490
20 changed files with 768 additions and 28 deletions

View File

@ -92,3 +92,12 @@ ldap:
ldap-group-dn: "ou=group,dc=eryajf,dc=net"
ldap-group-name-modify: false
ldap-user-name-modify: false
dingtalk:
ding-talk-app-key: "xxxxxx"
ding-talk-app-secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxx-vhIGL"
ding-talk-agent-id: "12121212"
ding-talk-root-ou-name: "钉钉首个部门的名称"
#为了方便数据库存储防止第三方id重复故而增加一个前缀用于用户表和分组表中第三方id存储,加上此处配置的source字段进行区分来源判断唯一。长度不超过10.
#因为分组表不可能成为性能瓶颈,故而不再拆分到新的关系表去维护第三方信息,用户表设计同理
ding-talk-id-source: "dingtalk"
ding-talk-user-init-password: "dingding@123"

View File

@ -24,6 +24,7 @@ type config struct {
RateLimit *RateLimitConfig `mapstructure:"rate-limit" json:"rateLimit"`
Ldap *LdapConfig `mapstructure:"ldap" json:"ldap"`
Email *EmailConfig `mapstructure:"email" json:"email"`
DingTalk *DingTalkConfig `mapstructure:"dingtalk" json:"dingTalk"`
}
// 设置读取配置信息
@ -144,3 +145,12 @@ type EmailConfig struct {
Pass string `mapstructure:"pass" json:"pass"`
From string `mapstructure:"from" json:"from"`
}
type DingTalkConfig struct {
DingTalkAppKey string `mapstructure:"ding-talk-app-key" json:"dingTalkAppKey"`
DingTalkAppSecret string `mapstructure:"ding-talk-app-secret" json:"dingTalkAppSecret"`
DingTalkAgentId string `mapstructure:"ding-talk-agent-id" json:"dingTalkAgentId"`
DingTalkRootOuName string `mapstructure:"ding-talk-root-ou-name" json:"dingTalkRootOuName"`
DingTalkIdSource string `mapstructure:"ding-talk-id-source" json:"dingTalkIdSource"`
DingTalkUserInitPassword string `mapstructure:"ding-talk-user-init-password" json:"dingTalkUserInitPassword"`
}

View File

@ -80,3 +80,11 @@ func (m *GroupController) RemoveUser(c *gin.Context) {
return logic.Group.RemoveUser(c, req)
})
}
//同步钉钉部门信息
func (m *GroupController) SyncDingTalkDepts(c *gin.Context) {
req := new(request.SyncDingTalkDeptsReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.DingTalk.DsyncDingTalkDepts(c, req)
})
}

View File

@ -64,3 +64,11 @@ func (uc UserController) GetUserInfo(c *gin.Context) {
return logic.User.GetUserInfo(c, req)
})
}
// 同步钉钉用户信息
func (uc UserController) SyncDingTalkUsers(c *gin.Context) {
req := new(request.SyncDingUserReq)
Run(c, req, func() (interface{}, interface{}) {
return logic.DingTalk.SyncDingTalkUsers(c, req)
})
}

2
go.mod
View File

@ -14,8 +14,10 @@ require (
github.com/go-playground/validator/v10 v10.9.0
github.com/juju/ratelimit v1.0.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/robfig/cron/v3 v3.0.0
github.com/spf13/viper v1.7.1
github.com/thoas/go-funk v0.7.0
github.com/zhaoyunxing92/dingtalk/v2 v2.0.7-0.20220601083444-173c10c3f835
go.uber.org/zap v1.19.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/natefinch/lumberjack.v2 v2.0.0

10
go.sum
View File

@ -298,8 +298,9 @@ github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNC
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -314,6 +315,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -381,6 +384,8 @@ github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnj
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zhaoyunxing92/dingtalk/v2 v2.0.7-0.20220601083444-173c10c3f835 h1:T6/rI54b4nVpQlIDv0iB0hTff4hzlXe63QcBcZ3u73s=
github.com/zhaoyunxing92/dingtalk/v2 v2.0.7-0.20220601083444-173c10c3f835/go.mod h1:MSvHUbYR94ffuWbJKFb8yHYyHg3qC/kQ3Hqpr6lK5ko=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@ -582,9 +587,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -15,5 +15,6 @@ var (
Role = &RoleLogic{}
Menu = &MenuLogic{}
OperationLog = &OperationLogLogic{}
DingTalk = &DingTalkLogic{}
Base = &BaseLogic{}
)

535
logic/dingtakl_logic.go Normal file
View File

@ -0,0 +1,535 @@
package logic
import (
"errors"
"fmt"
"github.com/eryajf/go-ldap-admin/config"
"github.com/eryajf/go-ldap-admin/model"
"github.com/eryajf/go-ldap-admin/public/tools"
"github.com/eryajf/go-ldap-admin/service/ildap"
"github.com/eryajf/go-ldap-admin/service/isql"
"github.com/eryajf/go-ldap-admin/svc/request"
"github.com/gin-gonic/gin"
"github.com/zhaoyunxing92/dingtalk/v2"
dingreq "github.com/zhaoyunxing92/dingtalk/v2/request"
"gorm.io/gorm"
"strconv"
"strings"
)
type DingTalkLogic struct {
}
//通过钉钉获取部门信息
func (d *DingTalkLogic) DsyncDingTalkDepts(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
client, err := dingtalk.NewClient(config.Conf.DingTalk.DingTalkAppKey, config.Conf.DingTalk.DingTalkAppSecret)
// 先存根部门信息到数据库和ldap钉钉根部门id为1ldap根部门名称为config.Conf.DingTalk.DingTalkRootOu
r := request.DingGroupAddReq{}
r.GroupName = config.Conf.DingTalk.DingTalkRootOuName
r.GroupType = "ou"
r.ParentId = 0
r.Remark = "钉钉根部门"
r.Source = config.Conf.DingTalk.DingTalkIdSource
r.SourceDeptId = fmt.Sprintf("%s_%s", config.Conf.DingTalk.DingTalkIdSource, "1")
r.SourceDeptParentId = fmt.Sprintf("%s_%s", config.Conf.DingTalk.DingTalkIdSource, "0")
group, err := d.AddDept(&r)
if err != nil {
return nil, fmt.Sprintf("新增部门失败:部门名称为:%s,钉钉部门id为%d,错误信息:%s", r.GroupName, r.SourceDeptId, err.Error())
}
// 获取根部门下的部门信息,进行处理
reqDept := &dingreq.DeptList{}
reqDept.DeptId = 1
reqDept.Language = "zh_CN"
err = d.GetSubDepts(client, reqDept, group.ID, r.Source)
if err != nil {
return nil, fmt.Sprintf("DsyncDingTalkDepts同步部门出错%s", err.Error())
}
return nil, nil
}
// 通过钉钉获取部门信息,并存入数据库
func (d *DingTalkLogic) GetSubDepts(client *dingtalk.DingTalk, req *dingreq.DeptList, pgId uint, source string) error {
// 获取子部门列表
depts, err := client.GetDeptList(req)
if err != nil {
return errors.New(fmt.Sprintf("GetSubDepts获取部门列表失败%s", err.Error()))
}
fmt.Println("GetSubDepts获取到的钉钉部门列表", depts)
// 遍历并处理当前部门信息
for _, dept := range depts.Depts {
//先判断分组类型
localDept := request.DingGroupAddReq{
GroupType: "ou",
ParentId: pgId,
GroupName: dept.Name,
Remark: dept.Name,
Source: config.Conf.DingTalk.DingTalkIdSource,
SourceDeptParentId: fmt.Sprintf("%s_%d", source, dept.ParentId),
SourceDeptId: fmt.Sprintf("%s_%d", source, dept.Id),
SourceUserNum: 0,
}
//获取钉钉方若部门存在人员信息则设置为cn类型
reqTemp := &dingreq.DeptUserId{}
reqTemp.DeptId = dept.Id
repTemp, err := client.GetDeptUserIds(reqTemp)
if err != nil {
return errors.New(fmt.Sprintf("GetSubDepts获取部门用户Id列表失败%s", err.Error()))
}
fmt.Println("钉钉部门人员列表:", repTemp)
if len(repTemp.UserIds) > 0 {
localDept.GroupType = "cn"
localDept.SourceUserNum = len(repTemp.UserIds)
}
// 处理部门入库
deptTemp, err := d.AddDept(&localDept)
if err != nil {
return errors.New(fmt.Sprintf("GetSubDepts添加部门入库失败%s", err.Error()))
}
// 递归调用
sub := &dingreq.DeptList{}
sub.DeptId = dept.Id
sub.Language = "zh_CN"
d.GetSubDepts(client, sub, deptTemp.ID, deptTemp.Source)
}
return nil
}
//根据现有数据库同步到的部门信息,开启用户同步
func (d DingTalkLogic) SyncDingTalkUsers(c *gin.Context, req interface{}) (data interface{}, rspError interface{}) {
client, err := dingtalk.NewClient(config.Conf.DingTalk.DingTalkAppKey, config.Conf.DingTalk.DingTalkAppSecret)
//获取数据库里面的钉钉同步过来的部门信息
r := request.GroupListAllReq{}
r.GroupType = "cn"
r.Source = config.Conf.DingTalk.DingTalkIdSource
depts, err := isql.Group.ListAll(&r)
if err != nil {
return nil, fmt.Sprintf("SyncDingTalkUsers查询本地部门列表失败", err.Error())
}
//遍历处理部门,获取钉钉对应的用户信息
for index, dept := range depts {
fmt.Println(fmt.Sprintf("当前进行的步调为:%d,部门名称为:%s", index, dept.GroupName))
err = d.AddDeptUser(client, dept, 0)
if err != nil {
return nil, fmt.Sprintf("SyncDingTalkUsers添加部门下用户失败", err.Error())
}
}
return nil, nil
}
//获取并处理钉钉部门下的用户信息入库
func (d DingTalkLogic) AddDeptUser(client *dingtalk.DingTalk, dept *model.Group, cursor int) error {
// 处理部门下的人员信息
deptId := strings.Split(dept.SourceDeptId, "_")
tempId, err := strconv.Atoi(deptId[1])
if err != nil {
return err
}
//方式一获取部门下用户信息一次100个遍历后插入数据库经过验证第三方依赖包有问题
r := dingreq.DeptDetailUserInfo{}
r.DeptId = tempId
r.Language = "zh_CN"
r.Cursor = cursor
r.Size = 100
//获取钉钉部门人员信息
rep, err := client.GetDeptDetailUserInfo(&r)
fmt.Println(fmt.Sprintf("当前获取的部门名称为:%s,总用户量为:%d", dept.GroupName, len(rep.DeptDetailUsers)))
if err != nil {
return errors.New(fmt.Sprintf("AddDeptUser获取钉钉部门人员信息失败%s", err.Error()))
}
//方式二临时处理方案获取部门用户id列表遍历挨个从钉钉获取用户信息
//dingr := dingreq.DeptUserId{}
//dingr.DeptId = tempId
//deptUserIds, err := client.GetDeptUserIds(&dingr)
//if err != nil {
// return errors.New(fmt.Sprintf("AddDeptUser通过用户部门id从钉钉获取用户id列表失败:%s", err.Error()))
//}
// 遍历并处理当前部门下的人员信息
for _, detail := range rep.DeptDetailUsers {
//for index, userId := range deptUserIds.UserIds {
// fmt.Println(fmt.Sprintf("获取到的部门用户数为:%d,正在处理的用户序号为:%d,总Ids为", len(deptUserIds.UserIds), index))
// fmt.Println(deptUserIds.UserIds)
// userReq := dingreq.UserDetail{}
// userReq.UserId = userId
// userReq.Language = "zh_CN"
// detail, err := client.GetUserDetail(&userReq)
// if err != nil {
// return errors.New(fmt.Sprintf("AddDeptUser通过用户id从钉钉获取用户详情失败:%s", err.Error()))
// }
// 获取人员信息
fmt.Println("钉钉人员详情:", detail)
userName := detail.Mobile
if detail.OrgEmail != "" {
emailstr := strings.Split(detail.OrgEmail, "@")
userName = emailstr[0]
}
//钉钉部门ids,转换为内部部门id
sourceDeptIds := []string{}
for _, deptId := range detail.DeptIds {
sourceDeptIds = append(sourceDeptIds, fmt.Sprintf("%s_%d", config.Conf.DingTalk.DingTalkIdSource, deptId))
}
groupIds, err := isql.Group.DingTalkDeptIdsToGroupIds(sourceDeptIds)
if err != nil {
return errors.New(fmt.Sprintf("AddDeptUser转换钉钉部门id到本地分组id出错%s", err.Error()))
}
user := request.DingUserAddReq{
Username: userName,
Password: config.Conf.DingTalk.DingTalkUserInitPassword,
Nickname: detail.Name,
GivenName: detail.Name,
Mail: detail.OrgEmail,
JobNumber: detail.JobNumber,
Mobile: detail.Mobile,
Avatar: detail.Avatar,
PostalAddress: detail.WorkPlace,
Departments: dept.GroupName,
Position: detail.Title,
Introduction: detail.Remark,
Status: 1,
DepartmentId: groupIds,
Source: config.Conf.DingTalk.DingTalkIdSource,
SourceUserId: fmt.Sprintf("%s_%s", config.Conf.DingTalk.DingTalkIdSource, detail.UserId),
SourceUnionId: fmt.Sprintf("%s_%s", config.Conf.DingTalk.DingTalkIdSource, detail.UnionId),
}
// 入库
repUser, err := d.AddUser(&user)
if err != nil {
return errors.New(fmt.Sprintf("AddDeptUser添加用户失败%s", err.Error()))
}
fmt.Println("入库成功,用户信息为:")
fmt.Println(repUser)
}
if rep.HasMore {
err = d.AddDeptUser(client, dept, rep.NextCursor)
if err != nil {
return errors.New(fmt.Sprintf("AddDeptUser添加用户失败%s", err.Error()))
}
}
return nil
}
// AddGroup 添加部门数据
func (d DingTalkLogic) AddDept(r *request.DingGroupAddReq) (data *model.Group, rspError error) {
// 判断部门名称是否存在
filter := tools.H{"source_dept_id": r.SourceDeptId}
dept := new(model.Group)
err := isql.Group.Find(filter, dept)
flag := errors.Is(err, gorm.ErrRecordNotFound)
fmt.Println("部门是否存在:", filter, flag)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New(fmt.Sprintf("AddDept添加部门失败%s", err.Error()))
}
//分组不存在直接创建此处通过部门名称和第三方id来共同判定唯一理论上不会出现重复
if errors.Is(err, gorm.ErrRecordNotFound) {
group := model.Group{
GroupType: r.GroupType,
ParentId: r.ParentId,
GroupName: r.GroupName,
Remark: r.Remark,
Creator: "system",
Source: r.Source,
SourceDeptParentId: r.SourceDeptParentId,
SourceDeptId: r.SourceDeptId,
SourceUserNum: r.SourceUserNum,
}
pdn := ""
if group.ParentId > 0 {
pdn, err = isql.Group.GetGroupDn(r.ParentId, "")
if err != nil {
return nil, errors.New(fmt.Sprintf("AddDept获取父级部门dn失败%s", err.Error()))
}
}
err = ildap.Group.Add(&group, pdn)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("AddDept向LDAP创建分组失败" + err.Error()))
}
// 创建
err = isql.Group.Add(&group)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("AddDept向MySQL创建分组失败:" + err.Error()))
}
// 默认创建分组之后需要将admin添加到分组中
adminInfo := new(model.User)
err = isql.User.Find(tools.H{"id": 1}, adminInfo)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddDept获取admin用户失败%s", tools.NewMySqlError(err).Error()))
}
err = isql.Group.AddUserToGroup(&group, []model.User{*adminInfo})
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("AddDept添加用户到分组失败: %s", err.Error()))
}
return &group, nil
} else { //分组存在
//判断是否名字/备注/钉钉部门ID有修改
if r.GroupName != dept.GroupName || r.Remark != dept.Remark || r.SourceDeptParentId != dept.SourceDeptParentId || r.SourceUserNum != dept.SourceUserNum {
err = d.UpdateDept(r)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddDept更新部门失败%s", err.Error()))
}
}
//处理父级部门变化
if r.SourceDeptParentId != dept.SourceDeptParentId {
// TODO 待处理父级部门变化情况
}
return dept, nil
}
}
// UpdateDept 更新部门数据
func (d DingTalkLogic) UpdateDept(r *request.DingGroupAddReq) error {
oldData := new(model.Group)
filter := tools.H{"source_dept_id": r.SourceDeptId}
err := isql.Group.Find(filter, oldData)
if err != nil {
return errors.New(fmt.Sprintf("UpdateDept获取旧的部门信息失败:%s", tools.NewMySqlError(err).Error()))
}
dept := model.Group{
Model: oldData.Model,
GroupName: r.GroupName,
Remark: r.Remark,
Creator: "system",
GroupType: oldData.GroupType,
SourceDeptId: r.SourceDeptId,
SourceDeptParentId: r.SourceDeptParentId,
SourceUserNum: r.SourceUserNum,
}
oldGroupName := oldData.GroupName
oldRemark := oldData.Remark
dn, err := isql.Group.GetGroupDn(oldData.ID, "")
if err != nil {
return errors.New(fmt.Sprintf("UpdateDept不去部门dn失败:%s", tools.NewMySqlError(err).Error()))
}
err = ildap.Group.Update(&dept, dn, oldGroupName, oldRemark)
if err != nil {
return tools.NewLdapError(fmt.Errorf("UpdateDept向LDAP更新分组失败" + err.Error()))
}
//若配置了不允许修改分组名称,则不更新分组名称
if !config.Conf.Ldap.LdapGroupNameModify {
dept.GroupName = oldGroupName
}
err = isql.Group.Update(&dept)
if err != nil {
return tools.NewLdapError(fmt.Errorf("UpdateDept向MySQL更新分组失败:" + err.Error()))
}
return nil
}
// AddUser 添加用户数据
func (d DingTalkLogic) AddUser(r *request.DingUserAddReq) (data *model.User, rspError error) {
isExist := false
oldData := new(model.User)
if isql.User.Exist(tools.H{"source_user_id": r.SourceUserId}) {
err := isql.User.Find(tools.H{"source_user_id": r.SourceUserId}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户id获取用户失败%s", err.Error()))
}
isExist = true
}
if !isExist {
if isql.User.Exist(tools.H{"source_union_id": r.SourceUnionId}) {
err := isql.User.Find(tools.H{"source_union_id": r.SourceUnionId}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户unionid获取用户失败%s", err.Error()))
}
isExist = true
}
}
if !isExist {
if r.Mail != "" && isql.User.Exist(tools.H{"mail": r.Mail}) {
err := isql.User.Find(tools.H{"mail": r.Mail}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户mail获取用户失败%s", err.Error()))
}
isExist = true
}
}
if !isExist {
if isql.User.Exist(tools.H{"job_number": r.JobNumber}) {
err := isql.User.Find(tools.H{"job_number": r.JobNumber}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户job_number获取用户失败%s", err.Error()))
}
isExist = true
}
}
if !isExist {
if isql.User.Exist(tools.H{"username": r.Username}) {
err := isql.User.Find(tools.H{"username": r.Username}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户username获取用户失败%s", err.Error()))
}
isExist = true
}
}
if !isExist {
if isql.User.Exist(tools.H{"mobile": r.Mobile}) {
err := isql.User.Find(tools.H{"mobile": r.Mobile}, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据钉钉用户mobile获取用户失败%s", err.Error()))
}
isExist = true
}
}
if isExist {
user, err := d.UpdateUser(r, oldData)
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser用户已存在更新用户失败%s", err.Error()))
}
return user, nil
}
// 根据角色id获取角色
r.RoleIds = []uint{2} // 默认添加为普通用户角色
roles, err := isql.Role.GetRolesByIds(r.RoleIds)
if err != nil {
return nil, tools.NewValidatorError(fmt.Errorf("AddUser根据角色ID获取角色信息失败:%s", err.Error()))
}
var reqRoleSorts []int
for _, role := range roles {
reqRoleSorts = append(reqRoleSorts, int(role.Sort))
}
deptIds := tools.SliceToString(r.DepartmentId, ",")
user := model.User{
Username: r.Username,
Password: r.Password,
Nickname: r.Nickname,
GivenName: r.GivenName,
Mail: r.Mail,
JobNumber: r.JobNumber,
Mobile: r.Mobile,
Avatar: r.Avatar,
PostalAddress: r.PostalAddress,
Departments: r.Departments,
Position: r.Position,
Introduction: r.Introduction,
Status: r.Status,
Creator: "system",
DepartmentId: deptIds,
Roles: roles,
Source: r.Source,
SourceUserId: r.SourceUserId,
SourceUnionId: r.SourceUnionId,
}
if user.Introduction == "" {
user.Introduction = r.Nickname
}
if user.JobNumber == "" {
user.JobNumber = r.Mobile
}
//先识别用户选择的部门是否是OU开头
gdns := make(map[uint]string)
for _, deptId := range r.DepartmentId {
dn, err := isql.Group.GetGroupDn(deptId, "")
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser根据用户dn信息失败:%s", err.Error()))
}
gdn := fmt.Sprintf("%s,%s", dn, config.Conf.Ldap.LdapBaseDN)
if gdn[:3] == "ou=" {
return nil, errors.New(fmt.Sprintf("AddUser不能添加用户到OU组织单元:%s", gdn))
}
gdns[deptId] = gdn
}
//先创建用户到默认分组
err = ildap.User.Add(&user)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("AddUser向LDAP创建用户失败" + err.Error()))
}
isExistUser := false
for deptId, gdn := range gdns {
//根据选择的部门,添加到部门内
err = ildap.Group.AddUserToGroup(gdn, fmt.Sprintf("uid=%s,%s", user.Username, config.Conf.Ldap.LdapUserDN))
if err != nil {
return nil, errors.New(fmt.Sprintf("AddUser向部门添加用户失败%s", err.Error()))
}
if !isExistUser {
err = isql.User.Add(&user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("向MySQL创建用户失败" + err.Error()))
}
isExistUser = true
}
//根据部门分配,将用户和部门信息维护到部门关系表里面
users := []model.User{}
users = append(users, user)
depart := new(model.Group)
filter := tools.H{"id": deptId}
err = isql.Group.Find(filter, depart)
if err != nil {
return nil, tools.NewMySqlError(err)
}
err = isql.Group.AddUserToGroup(depart, users)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("AddUser向MySQL添加用户到分组关系失败" + err.Error()))
}
}
return &user, nil
}
// Update 更新数据
func (d DingTalkLogic) UpdateUser(r *request.DingUserAddReq, oldData *model.User) (data *model.User, rspError error) {
deptIds := tools.SliceToString(r.DepartmentId, ",")
user := model.User{
Model: oldData.Model,
Username: r.Username,
Nickname: r.Nickname,
GivenName: r.GivenName,
Mail: r.Mail,
JobNumber: r.JobNumber,
Mobile: r.Mobile,
Avatar: r.Avatar,
PostalAddress: r.PostalAddress,
Departments: r.Departments,
Position: r.Position,
Introduction: r.Introduction,
Creator: "system",
DepartmentId: deptIds,
Source: oldData.Source,
Roles: oldData.Roles,
SourceUserId: r.SourceUserId,
SourceUnionId: r.SourceUnionId,
}
if user.Introduction == "" {
user.Introduction = r.Nickname
}
if user.PostalAddress == "" {
user.PostalAddress = "没有填写地址"
}
if user.Position == "" {
user.Position = "技术"
}
if user.JobNumber == "" {
user.JobNumber = r.Mobile
}
err := ildap.User.Update(oldData.Username, &user)
if err != nil {
return nil, tools.NewLdapError(fmt.Errorf("UpdateUser在LDAP更新用户失败" + err.Error()))
}
// 更新用户
if !config.Conf.Ldap.LdapUserNameModify {
user.Username = oldData.Username
}
err = isql.User.Update(&user)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("UpdateUser在MySQL更新用户失败" + err.Error()))
}
//判断部门信息是否有变化有变化则更新相应的数据库
oldDeptIds := tools.StringToSlice(oldData.DepartmentId, ",")
addDeptIds, removeDeptIds := tools.ArrUintCmp(oldDeptIds, r.DepartmentId)
for _, deptId := range removeDeptIds {
//从旧组中删除
err = User.RemoveUserToGroup(deptId, []uint{oldData.ID})
if err != nil {
return nil, errors.New(fmt.Sprintf("UpdateUser将用户从分组移除失败%s", err.Error()))
}
}
for _, deptId := range addDeptIds {
//添加到新分组中
err = User.AddUserToGroup(deptId, []uint{oldData.ID})
if err != nil {
return nil, errors.New(fmt.Sprintf("UpdateUser将用户添加至分组失败%s", err.Error()))
}
}
return &user, nil
}

View File

@ -37,11 +37,14 @@ func (l GroupLogic) Add(c *gin.Context, req interface{}) (data interface{}, rspE
}
group := model.Group{
GroupType: r.GroupType,
ParentId: r.ParentId,
GroupName: r.GroupName,
Remark: r.Remark,
Creator: ctxUser.Username,
GroupType: r.GroupType,
ParentId: r.ParentId,
GroupName: r.GroupName,
Remark: r.Remark,
Creator: ctxUser.Username,
Source: "platform", //默认是平台添加
SourceDeptId: "platform_0",
SourceDeptParentId: "platform_0",
}
pdn := ""
if group.ParentId > 0 {
@ -114,7 +117,7 @@ func (l GroupLogic) GetTree(c *gin.Context, req interface{}) (data interface{},
_ = c
var groups []*model.Group
groups, err := isql.Group.List(r)
groups, err := isql.Group.ListTree(r)
if err != nil {
return nil, tools.NewMySqlError(fmt.Errorf("获取资源列表失败: " + err.Error()))
}
@ -219,6 +222,7 @@ func (l GroupLogic) Delete(c *gin.Context, req interface{}) (data interface{}, r
return nil, tools.NewMySqlError(fmt.Errorf("删除接口失败: %s", err.Error()))
}
// TODO: 删除用户组关系
return nil, nil
}

15
main.go
View File

@ -3,6 +3,8 @@ package main
import (
"context"
"fmt"
"github.com/eryajf/go-ldap-admin/logic"
"github.com/robfig/cron/v3"
"net/http"
"os"
"os/signal"
@ -63,6 +65,19 @@ func main() {
common.Log.Fatalf("listen: %s\n", err)
}
}()
//启动定时任务
c := cron.New(cron.WithSeconds())
c.AddFunc("0 1 0 * * *", func() {
common.Log.Info("每天0点1分0秒执行一次同步钉钉部门和用户信息到ldap")
logic.DingTalk.DsyncDingTalkDepts(nil, nil)
})
//每天凌晨1点执行一次
c.AddFunc("0 15 0 * * *", func() {
common.Log.Info("每天凌晨00点15分执行一次同步钉钉部门和用户信息到ldap")
logic.DingTalk.SyncDingTalkUsers(nil, nil)
})
c.Start()
common.Log.Info(fmt.Sprintf("Server is running at %s:%d/%s", host, port, config.Conf.System.UrlPathPrefix))

View File

@ -4,11 +4,15 @@ import "gorm.io/gorm"
type Group struct {
gorm.Model
GroupName string `gorm:"type:varchar(20);comment:'分组名称'" json:"groupName"`
Remark string `gorm:"type:varchar(100);comment:'分组中文说明'" json:"remark"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
GroupType string `gorm:"type:varchar(20);comment:'分组类型cn、ou'" json:"groupType"`
Users []*User `gorm:"many2many:group_users" json:"users"`
ParentId uint `gorm:"default:0;comment:'父组编号(编号为0时表示根组)'" json:"parentId"`
Children []*Group `gorm:"-" json:"children"`
GroupName string `gorm:"type:varchar(20);comment:'分组名称'" json:"groupName"`
Remark string `gorm:"type:varchar(100);comment:'分组中文说明'" json:"remark"`
Creator string `gorm:"type:varchar(20);comment:'创建人'" json:"creator"`
GroupType string `gorm:"type:varchar(20);comment:'分组类型cn、ou'" json:"groupType"`
Users []*User `gorm:"many2many:group_users" json:"users"`
ParentId uint `gorm:"default:0;comment:'父组编号(编号为0时表示根组)'" json:"parentId"`
SourceDeptId string `gorm:"type:varchar(100);comment:'部门编号'" json:"sourceDeptId"`
Source string `gorm:"type:varchar(20);comment:'来源dingTalk、weCom、ldap、platform'" json:"source"`
SourceDeptParentId string `gorm:"type:varchar(100);comment:'父部门编号'" json:"sourceDeptParentId"`
SourceUserNum int `gorm:"default:0;comment:'部门下的用户数量,从第三方获取的数据'" json:"source_user_num"`
Children []*Group `gorm:"-" json:"children"`
}

View File

@ -21,4 +21,6 @@ type User struct {
Source string `gorm:"type:varchar(50);comment:'用户来源dingTalk、weCom、ldap、platform'" json:"source"` // 来源
DepartmentId string `gorm:"type:varchar(100);not null;comment:'部门id'" json:"departmentId"` // 部门id
Roles []*Role `gorm:"many2many:user_roles" json:"roles"` // 角色
SourceUserId string `gorm:"type:varchar(100);not null;comment:'第三方用户id'" json:"sourceUserId"` // 第三方用户id
SourceUnionId string `gorm:"type:varchar(100);not null;comment:'第三方唯一unionId'" json:"sourceUnionId"` // 第三方唯一unionId
}

View File

@ -324,6 +324,13 @@ func InitData() {
Remark: "更改用户在职状态",
Creator: "系统",
},
{
Method: "POST",
Path: "/user/syncDingTalkUsers",
Category: "user",
Remark: "从钉钉拉取用户信息",
Creator: "系统",
},
{
Method: "GET",
Path: "/group/list",
@ -387,6 +394,13 @@ func InitData() {
Remark: "获取不在分组内的用户列表",
Creator: "系统",
},
{
Method: "POST",
Path: "/group/syncDingTalkDepts",
Category: "group",
Remark: "从钉钉拉取部门信息",
Creator: "系统",
},
{
Method: "GET",
Path: "/role/list",
@ -555,10 +569,12 @@ func InitData() {
"/user/info",
"/user/list",
"/user/changePwd",
"/user/syncDingTalkUsers",
"/group/list",
"/group/tree",
"/group/useringroup",
"/group/usernoingroup",
"/group/syncDingTalkDepts",
"/role/list",
"/role/getmenulist",
"/role/getapilist",

View File

@ -25,6 +25,7 @@ func InitGroupRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) g
group.GET("/useringroup", controller.Group.UserInGroup)
group.GET("/usernoingroup", controller.Group.UserNoInGroup)
group.POST("/syncDingTalkDepts", controller.Group.SyncDingTalkDepts)
}
return r

View File

@ -16,13 +16,14 @@ func InitUserRoutes(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) gi
// 开启casbin鉴权中间件
user.Use(middleware.CasbinMiddleware())
{
user.GET("/info", controller.User.GetUserInfo) // 暂时未完成
user.GET("/list", controller.User.List) // 用户列表
user.POST("/add", controller.User.Add) // 添加用户
user.POST("/update", controller.User.Update) // 更新用户
user.POST("/delete", controller.User.Delete) // 删除用户
user.POST("/changePwd", controller.User.ChangePwd) // 修改用户密码
user.POST("/changeUserStatus", controller.User.ChangeUserStatus) // 修改用户状态
user.GET("/info", controller.User.GetUserInfo) // 暂时未完成
user.GET("/list", controller.User.List) // 用户列表
user.POST("/add", controller.User.Add) // 添加用户
user.POST("/update", controller.User.Update) // 更新用户
user.POST("/delete", controller.User.Delete) // 删除用户
user.POST("/changePwd", controller.User.ChangePwd) // 修改用户密码
user.POST("/changeUserStatus", controller.User.ChangeUserStatus) // 修改用户状态
user.POST("/syncDingTalkUsers", controller.User.SyncDingTalkUsers) // 同步用户
}
return r
}

View File

@ -31,8 +31,8 @@ func (x UserService) Add(user *model.User) error {
}
add := ldap.NewAddRequest(fmt.Sprintf("uid=%s,%s", user.Username, config.Conf.Ldap.LdapUserDN), nil)
add.Attribute("objectClass", []string{"inetOrgPerson"})
add.Attribute("cn", []string{user.Nickname})
add.Attribute("sn", []string{user.Username})
add.Attribute("cn", []string{user.Username})
add.Attribute("sn", []string{user.Nickname})
add.Attribute("businessCategory", []string{user.Departments})
add.Attribute("departmentNumber", []string{user.Position})
add.Attribute("description", []string{user.Introduction})

View File

@ -34,6 +34,59 @@ func (s GroupService) List(req *request.GroupListReq) ([]*model.Group, error) {
return list, err
}
// List 获取数据列表
func (s GroupService) ListTree(req *request.GroupListReq) ([]*model.Group, error) {
var list []*model.Group
db := common.DB.Model(&model.Group{}).Order("created_at DESC")
groupName := strings.TrimSpace(req.GroupName)
if groupName != "" {
db = db.Where("group_name LIKE ?", fmt.Sprintf("%%%s%%", groupName))
}
groupRemark := strings.TrimSpace(req.Remark)
if groupRemark != "" {
db = db.Where("remark LIKE ?", fmt.Sprintf("%%%s%%", groupRemark))
}
pageReq := tools.NewPageOption(req.PageNum, req.PageSize)
err := db.Offset(pageReq.PageNum).Limit(pageReq.PageSize).Find(&list).Error
return list, err
}
// List 获取数据列表
func (s GroupService) ListAll(req *request.GroupListAllReq) ([]*model.Group, error) {
var list []*model.Group
db := common.DB.Model(&model.Group{}).Order("created_at DESC")
groupName := strings.TrimSpace(req.GroupName)
if groupName != "" {
db = db.Where("group_name LIKE ?", fmt.Sprintf("%%%s%%", groupName))
}
groupRemark := strings.TrimSpace(req.Remark)
if groupRemark != "" {
db = db.Where("remark LIKE ?", fmt.Sprintf("%%%s%%", groupRemark))
}
groupType := strings.TrimSpace(req.GroupType)
if groupType != "" {
db = db.Where("group_type = ?", groupType)
}
source := strings.TrimSpace(req.Source)
if source != "" {
db = db.Where("source = ?", source)
}
sourceDeptId := strings.TrimSpace(req.SourceDeptId)
if sourceDeptId != "" {
db = db.Where("source_dept_id = ?", sourceDeptId)
}
sourceDeptParentId := strings.TrimSpace(req.SourceDeptParentId)
if sourceDeptParentId != "" {
db = db.Where("source_dept_parent_id = ?", sourceDeptParentId)
}
err := db.Find(&list).Error
return list, err
}
// 拼装dn信息
func (s GroupService) GetGroupDn(groupId uint, oldDn string) (dn string, e error) {
depart := new(model.Group)
@ -121,3 +174,17 @@ func (s GroupService) AddUserToGroup(group *model.Group, users []model.User) err
func (s GroupService) RemoveUserFromGroup(group *model.Group, users []model.User) error {
return common.DB.Model(&group).Association("Users").Delete(users)
}
// DingTalkDeptIdsToGroupIds 将钉钉部门id转换为分组id
func (s GroupService) DingTalkDeptIdsToGroupIds(dingTalkIds []string) (groupIds []uint, err error) {
tempGroups := []model.Group{}
err = common.DB.Model(&model.Group{}).Where("source_dept_id IN (?)", dingTalkIds).Find(&tempGroups).Error
if err != nil {
return nil, err
}
tempGroupIds := []uint{}
for _, g := range tempGroups {
tempGroupIds = append(tempGroupIds, g.ID)
}
return tempGroupIds, nil
}

View File

@ -232,7 +232,7 @@ func (s UserService) ChangePwd(username string, hashNewPasswd string) error {
} else {
// 没有缓存就获取用户信息缓存
var user model.User
common.DB.Where("username = ?", username).First(&user)
common.DB.Where("username = ?", username).Preload("Roles").First(&user)
userInfoCache.Set(username, user, cache.DefaultExpiration)
}
}

View File

@ -8,6 +8,16 @@ type GroupListReq struct {
PageSize int `json:"pageSize" form:"pageSize"`
}
// GroupListAllReq 获取资源列表结构体,不分页
type GroupListAllReq struct {
GroupName string `json:"groupName" form:"groupName"`
GroupType string `json:"groupType" form:"groupType"`
Remark string `json:"remark" form:"remark"`
Source string `json:"source" form:"source"`
SourceDeptId string `json:"sourceDeptId"`
SourceDeptParentId string `json:"SourceDeptParentId"`
}
// GroupAddReq 添加资源结构体
type GroupAddReq struct {
GroupType string `json:"groupType" validate:"required,min=1,max=20"`
@ -17,6 +27,19 @@ type GroupAddReq struct {
Remark string `json:"remark" validate:"min=0,max=100"` // 分组的中文描述
}
// DingTalkGroupAddReq 添加钉钉资源结构体
type DingGroupAddReq struct {
GroupType string `json:"groupType" validate:"required,min=1,max=20"`
GroupName string `json:"groupName" validate:"required,min=1,max=20"`
//父级Id 大于等于0 必填
ParentId uint `json:"parentId" validate:"omitempty,min=0"`
Remark string `json:"remark" validate:"min=0,max=100"` // 分组的中文描述
SourceDeptId string `json:"sourceDeptId"`
Source string `json:"source"`
SourceDeptParentId string `json:"SourceDeptParentId"`
SourceUserNum int `json:"sourceUserNum"`
}
// GroupUpdateReq 更新资源结构体
type GroupUpdateReq struct {
ID uint `json:"id" form:"id" validate:"required"`
@ -58,3 +81,7 @@ type UserNoInGroupReq struct {
GroupID uint `json:"groupId" form:"groupId" validate:"required"`
Nickname string `json:"nickname" form:"nickname"`
}
// SyncDingTalkDeptsReq 同步钉钉部门信息
type SyncDingTalkDeptsReq struct {
}

View File

@ -6,7 +6,7 @@ type UserAddReq struct {
Password string `json:"password"`
Nickname string `json:"nickname" validate:"required,min=0,max=20"`
GivenName string `json:"givenName" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"required,min=0,max=20"`
Mail string `json:"mail" validate:"required,min=0,max=100"`
JobNumber string `json:"jobNumber" validate:"required,min=0,max=20"`
PostalAddress string `json:"postalAddress" validate:"min=0,max=255"`
Departments string `json:"departments" validate:"min=0,max=255"`
@ -20,13 +20,35 @@ type UserAddReq struct {
RoleIds []uint `json:"roleIds" validate:"required"`
}
// DingUserAddReq 钉钉用户创建资源结构体
type DingUserAddReq struct {
Username string `json:"username" validate:"required,min=2,max=20"`
Password string `json:"password"`
Nickname string `json:"nickname" validate:"required,min=0,max=20"`
GivenName string `json:"givenName" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"required,min=0,max=100"`
JobNumber string `json:"jobNumber" validate:"required,min=0,max=20"`
PostalAddress string `json:"postalAddress" validate:"min=0,max=255"`
Departments string `json:"departments" validate:"min=0,max=255"`
Position string `json:"position" validate:"min=0,max=255"`
Mobile string `json:"mobile" validate:"required,checkMobile"`
Avatar string `json:"avatar"`
Introduction string `json:"introduction" validate:"min=0,max=255"`
Status uint `json:"status" validate:"oneof=1 2"`
DepartmentId []uint `json:"departmentId" validate:"required"`
Source string `json:"source" validate:"min=0,max=20"`
RoleIds []uint `json:"roleIds" validate:"required"`
SourceUserId string `json:"sourceUserId"` // 第三方用户id
SourceUnionId string `json:"sourceUnionId"` // 第三方唯一unionId
}
// UserUpdateReq 更新资源结构体
type UserUpdateReq struct {
ID uint `json:"id" validate:"required"`
Username string `json:"username" validate:"required,min=2,max=20"`
Nickname string `json:"nickname" validate:"min=0,max=20"`
GivenName string `json:"givenName" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"min=0,max=20"`
Mail string `json:"mail" validate:"min=0,max=100"`
JobNumber string `json:"jobNumber" validate:"min=0,max=20"`
PostalAddress string `json:"postalAddress" validate:"min=0,max=255"`
Departments string `json:"departments" validate:"min=0,max=255"`
@ -60,6 +82,10 @@ type UserChangeUserStatusReq struct {
type UserGetUserInfoReq struct {
}
// 同步钉钉用户信息
type SyncDingUserReq struct {
}
// UserListReq 获取用户列表结构体
type UserListReq struct {
Username string `json:"username" form:"username"`