增加ai办公,考勤管理

This commit is contained in:
hero920103 committed 2026-11-14 18:15:55 +08:00
1 parent eb9b7beff5
commit 31ae07175d
46 files changed
+9250 -296

No files matched your search

+286
View File
@@ -0,0 +1,286 @@
// 老数据补齐:为「人员管理里有、用户管理里没有」的老员工补建登录账号。
//
// 背景:早期人员是直接在 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())
}