287 lines
8.3 KiB
Go
287 lines
8.3 KiB
Go
// 老数据补齐:为「人员管理里有、用户管理里没有」的老员工补建登录账号。
|
||
//
|
||
// 背景:早期人员是直接在 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())
|
||
}
|