Files
yunzerwebsiteallinone/go/scripts/syncemployeeusers/main.go
T

287 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 老数据补齐:为「人员管理里有、用户管理里没有」的老员工补建登录账号。
//
// 背景:早期人员是直接在 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())
}