// 老数据补齐:为「人员管理里有、用户管理里没有」的老员工补建登录账号。 // // 背景:早期人员是直接在 OA 人员管理里手工录入的(离线导入),没有对应的登录账号, // 于是「基础设置 → 用户管理」人少、「OA → 人员管理」人多,两边对不上。 // // 本脚本走的是和「用户管理 → 添加用户」完全相同的服务链路: // authsvc.SyncTenantUserAndGetIdentity(建/取统一身份,返回 identity_id) // → services.BindTenantUser(写 yz_system_tenant_user,uid = identity_id) // 员工档案已存在,按 (tid, account) 自动关联,不会重复建档、也不会丢部门/职位/排班。 // // 用法(在 go/ 目录下运行,读取 conf/app.conf): // // go run scripts/syncemployeeusers/main.go -tid 2 预演:只列出会建哪些账号 // go run scripts/syncemployeeusers/main.go -tid 2 -apply 真正创建账号并打印初始密码 // go run scripts/syncemployeeusers/main.go -tid 2 -apply -password "Abc@123456" // // 安全提示: // - 未指定 -password 时每个账号生成随机初始密码,执行完请立即转告本人并让其修改; // - 若员工手机号/邮箱已属于另一个身份,脚本会跳过并提示,避免把两个人错误合并。 package main import ( "crypto/rand" "flag" "fmt" "math/big" "os" "strconv" "strings" "server/models" "server/pkg/passwordutil" "server/services" authsvc "server/services/auth" beego "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/client/orm" ) func main() { tid := flag.Int64("tid", 0, "只处理指定企业;0 表示全部企业") fixedPwd := flag.String("password", "", "统一初始密码;留空则每个账号随机生成") apply := flag.Bool("apply", false, "真正写入(默认只预演)") flag.Parse() if err := beego.LoadAppConfig("ini", "conf/app.conf"); err != nil { fmt.Println("读取 conf/app.conf 失败:", err) os.Exit(1) } models.Init("") type pending struct { empID int64 tid int64 account string name string phone string email string gender int64 birth string dept string status int64 } // 员工侧:所有未删除员工 empQuery := models.Orm.QueryTable(new(models.BackendEmployee)).Filter("delete_time__isnull", true) if *tid > 0 { empQuery = empQuery.Filter("tid", *tid) } var employees []models.BackendEmployee if _, err := empQuery.OrderBy("tid", "id").All(&employees); err != nil && err != orm.ErrNoRows { fmt.Println("读取员工失败:", err) os.Exit(1) } // 用户侧:(tid, account) 已有的账号 userQuery := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("delete_time__isnull", true) if *tid > 0 { userQuery = userQuery.Filter("tid", *tid) } var users []models.SystemTenantUser if _, err := userQuery.All(&users); err != nil && err != orm.ErrNoRows { fmt.Println("读取用户失败:", err) os.Exit(1) } exists := make(map[string]struct{}, len(users)) for _, u := range users { if u.Account == nil { continue } exists[fmt.Sprintf("%d|%s", u.Tid, strings.TrimSpace(*u.Account))] = struct{}{} } var todo []pending for _, e := range employees { if e.Tid == nil || *e.Tid <= 0 { continue } account := strings.TrimSpace(e.Account) if account == "" { continue } if _, ok := exists[fmt.Sprintf("%d|%s", *e.Tid, account)]; ok { continue } item := pending{ empID: int64(e.ID), tid: int64(*e.Tid), account: account, name: strings.TrimSpace(e.Name), gender: int64(e.Gender), status: int64(e.AccountStatus), } if item.name == "" { item.name = account } if e.Phone != nil { item.phone = strings.TrimSpace(*e.Phone) } if e.Email != nil { item.email = strings.TrimSpace(*e.Email) } if e.Birthday != nil { item.birth = e.Birthday.Format("2006-01-02") } if e.Department != nil { item.dept = strings.TrimSpace(*e.Department) } todo = append(todo, item) } if len(todo) == 0 { fmt.Println("没有需要补建账号的老员工,两边已经一致。") return } fmt.Printf("待补建账号:%d 个\n", len(todo)) for _, item := range todo { fmt.Printf(" tid=%d 员工id=%d account=%-16s name=%s dept=%s phone=%s\n", item.tid, item.empID, item.account, item.name, item.dept, item.phone) } if !*apply { fmt.Println("\n预演结束(未写入)。确认无误后加 -apply 执行。") return } fmt.Println() created := 0 for _, item := range todo { if conflict := identityConflict(item.account, item.phone, item.email); conflict != "" { fmt.Printf(" 跳过 %s:%s\n", item.account, conflict) continue } plain := *fixedPwd if plain == "" { plain = randomPassword() } hashed, err := passwordutil.Hash(plain) if err != nil { fmt.Printf(" 跳过 %s:密码加密失败 %v\n", item.account, err) continue } uid, err := authsvc.SyncTenantUserAndGetIdentity(authsvc.SyncTenantUserInput{ Tid: uint64(item.tid), Account: item.account, Name: item.name, Phone: item.phone, Email: item.email, PasswordHash: hashed, Status: 1, OrgID: parseUint(item.dept), }) if err != nil { fmt.Printf(" 跳过 %s:创建统一身份失败 %v\n", item.account, err) continue } account := item.account name := item.name var phonePtr, emailPtr, birthPtr, pwdPtr *string if item.phone != "" { phonePtr = &item.phone } if item.email != "" { emailPtr = &item.email } if item.birth != "" { birthPtr = &item.birth } pwdPtr = &hashed var sexPtr *uint8 if item.gender > 0 { sex := uint8(item.gender) sexPtr = &sex } status := int8(1) if item.status == 0 { status = 0 } id, err := services.BindTenantUser( uint64(item.tid), uid, &account, &name, phonePtr, emailPtr, sexPtr, birthPtr, pwdPtr, 0, status, nil, ) if err != nil { fmt.Printf(" 跳过 %s:写入租户用户失败 %v\n", item.account, err) continue } // 部门:与员工档案保持一致(用户管理表单里的所属部门) if deptID := parseUint(item.dept); deptID > 0 { _, _ = models.Orm.QueryTable(new(models.SystemTenantUser)). Filter("id", id). Update(orm.Params{"org_id": deptID}) } created++ fmt.Printf(" 已创建:tid=%d account=%s name=%s 初始密码=%s(员工id=%d 已自动关联)\n", item.tid, item.account, item.name, plain, item.empID) } fmt.Printf("\n完成:新建 %d 个账号,跳过 %d 个\n", created, len(todo)-created) } // identityConflict 手机号/邮箱已被其他账号占用时返回原因,避免错误合并两个自然人 func identityConflict(account, phone, email string) string { if phone != "" { var rows []models.AuthIdentity _, _ = models.Orm.QueryTable(new(models.AuthIdentity)). Filter("mobile", phone).All(&rows) for _, r := range rows { if r.Account == nil || strings.TrimSpace(*r.Account) != account { return "手机号 " + phone + " 已属于其他统一身份(identity " + strconv.FormatUint(r.ID, 10) + ")" } } } if email != "" { var rows []models.AuthIdentity _, _ = models.Orm.QueryTable(new(models.AuthIdentity)). Filter("email", email).All(&rows) for _, r := range rows { if r.Account == nil || strings.TrimSpace(*r.Account) != account { return "邮箱 " + email + " 已属于其他统一身份(identity " + strconv.FormatUint(r.ID, 10) + ")" } } } return "" } func parseUint(v string) uint64 { n, err := strconv.ParseUint(strings.TrimSpace(v), 10, 64) if err != nil { return 0 } return n } // randomPassword 生成 12 位含大小写与数字的随机初始密码 func randomPassword() string { const lower = "abcdefghijkmnpqrstuvwxyz" const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ" const digits = "23456789" pool := lower + upper + digits buf := make([]byte, 12) for i := range buf { n, err := rand.Int(rand.Reader, big.NewInt(int64(len(pool)))) if err != nil { buf[i] = pool[i%len(pool)] continue } buf[i] = pool[n.Int64()] } // 保证至少各含一位,避免被前端复杂度校验拦住 buf[0] = upper[randIdx(len(upper))] buf[1] = lower[randIdx(len(lower))] buf[2] = digits[randIdx(len(digits))] return string(buf) } func randIdx(n int) int { v, err := rand.Int(rand.Reader, big.NewInt(int64(n))) if err != nil { return 0 } return int(v.Int64()) }