Files
nightingale/pkg/flashduty/sync_user.go
2025-07-28 14:36:08 +08:00

267 lines
6.4 KiB
Go

package flashduty
import (
"errors"
"strconv"
"strings"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/toolkits/pkg/logger"
)
func SyncUsersChange(ctx *ctx.Context, dbUsers []*models.User) error {
if !ctx.IsCenter {
return nil
}
appKey, err := models.ConfigsGetFlashDutyAppKey(ctx)
if err != nil {
return err
}
req := make(map[string]interface{})
req["limit"] = 100
userList, err := PostFlashDutyWithResp[Data]("/member/list", appKey, req)
if err != nil {
return err
}
total := userList.Total
items := []Item{}
for i := 0; i < total/100+1; i++ {
req["p"] = i
req["limit"] = 100
resp, err := PostFlashDutyWithResp[Data]("/member/list", appKey, req)
if err != nil {
return err
}
items = append(items, resp.Items...)
}
dutyUsers := make(map[int64]*models.User, len(items))
for i := range items {
if items[i].RefID != "" {
id, _ := strconv.ParseInt(items[i].RefID, 10, 64)
user := &models.User{
Username: items[i].MemberName,
Email: items[i].Email,
Phone: items[i].Phone,
Id: id,
}
dutyUsers[id] = user
}
}
dbUsersHas := sliceToMap(dbUsers)
delUsers := diffMap(dutyUsers, dbUsersHas)
fdDelUsers(appKey, delUsers)
addUsers := diffMap(dbUsersHas, dutyUsers)
if err := fdAddUsers(appKey, addUsers); err != nil {
return err
}
updateUser(appKey, dbUsersHas, dutyUsers)
return nil
}
func sliceToMap(dbUsers []*models.User) map[int64]*models.User {
m := make(map[int64]*models.User, len(dbUsers))
for _, user := range dbUsers {
m[user.Id] = user
}
return m
}
// in m1 and not in m2
func diffMap(m1, m2 map[int64]*models.User) []models.User {
var diff []models.User
for i := range m1 {
if _, ok := m2[i]; !ok {
diff = append(diff, *m1[i])
}
}
return diff
}
func updateUser(appKey string, m1, m2 map[int64]*models.User) {
for i := range m1 {
if _, ok := m2[i]; ok {
if m1[i].Email != m2[i].Email || !PhoneIsSame(m1[i].Phone, m2[i].Phone) || m1[i].Username != m2[i].Username {
var flashdutyUser User
flashdutyUser = User{
RefID: strconv.FormatInt(m1[i].Id, 10),
}
flashdutyUser.Updates = Updates{
Phone: m1[i].Phone,
Email: m1[i].Email,
MemberName: m1[i].Username,
RefID: strconv.FormatInt(m1[i].Id, 10),
}
err := flashdutyUser.UpdateMember(appKey)
if err != nil {
logger.Errorf("failed to update user: %v", err)
}
}
}
}
}
func PhoneIsSame(phone1, phone2 string) bool {
// 兼容不同国家/地区前缀,例如 +86、+1、+44 等,以及包含空格或短横线的格式
normalize := func(p string) string {
p = strings.TrimSpace(p)
p = strings.ReplaceAll(p, " ", "")
p = strings.ReplaceAll(p, "-", "")
p = strings.TrimPrefix(p, "+")
return p
}
p1 := normalize(phone1)
p2 := normalize(phone2)
if p1 == p2 {
return true
}
// 如果长度相差不超过 3 且较长的以较短的结尾,则认为是相同号码(忽略最多 3 位国家区号差异)
if len(p1) > len(p2) {
return len(p1)-len(p2) <= 3 && strings.HasSuffix(p1, p2)
}
return len(p2)-len(p1) <= 3 && strings.HasSuffix(p2, p1)
}
type User struct {
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
MemberName string `json:"member_name,omitempty"`
RefID string `json:"ref_id,omitempty"`
Updates Updates `json:"updates,omitempty"`
}
type Updates struct {
RefID string `json:"ref_id,omitempty"`
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
MemberName string `json:"member_name,omitempty"`
CountryCode string `json:"country_code,omitempty"`
}
func (user *User) delMember(appKey string) error {
if user.RefID == "" {
return errors.New("refID must not be empty")
}
userDel := &User{RefID: user.RefID}
return PostFlashDuty("/member/delete", appKey, userDel)
}
func (user *User) UpdateMember(appKey string) error {
return PostFlashDuty("/member/info/reset", appKey, user)
}
type Members struct {
Users []User `json:"members"`
}
func (m *Members) addMembers(appKey string) error {
if len(m.Users) == 0 {
return nil
}
validUsers := make([]User, 0, len(m.Users))
for _, user := range m.Users {
if user.RefID == "" || (user.Phone == "" && user.Email == "") {
logger.Errorf("user(%+v) refID must not be none, Email or Phone can not be none", user)
} else {
validUsers = append(validUsers, user)
}
}
if len(validUsers) == 0 {
return nil
}
m.Users = validUsers
return PostFlashDuty("/member/invite", appKey, m)
}
func fdAddUsers(appKey string, users []models.User) error {
fdUsers := usersToFdUsers(users)
members := &Members{
Users: fdUsers,
}
return members.addMembers(appKey)
}
func fdDelUsers(appKey string, users []models.User) {
fdUsers := usersToFdUsers(users)
for _, fdUser := range fdUsers {
if err := fdUser.delMember(appKey); err != nil {
logger.Errorf("failed to delete user: %v", err)
}
}
}
func usersToFdUsers(users []models.User) []User {
fdUsers := make([]User, 0, len(users))
for i := range users {
fdUsers = append(fdUsers, User{
RefID: strconv.FormatInt(users[i].Id, 10),
Phone: users[i].Phone,
Email: users[i].Email,
MemberName: users[i].Username,
})
}
return fdUsers
}
func UpdateUser(ctx *ctx.Context, target models.User, email, phone string) {
//contact := target.FindSameContact(email, phone)
if target.Id == 0 {
logger.Errorf("user not found: %s", target.Username)
return
}
if email == "" && phone == "" {
logger.Errorf("email and phone are both empty: %s", target.Username)
return
}
var flashdutyUser User
refID := strconv.FormatInt(target.Id, 10)
flashdutyUser = User{
RefID: refID,
}
flashdutyUser.Updates = Updates{
Phone: phone,
Email: email,
MemberName: target.Username,
RefID: refID,
}
appKey, err := models.ConfigsGetFlashDutyAppKey(ctx)
if err != nil {
logger.Errorf("failed to get flashduty app key: %v", err)
return
}
err = flashdutyUser.UpdateMember(appKey)
if err != nil && strings.Contains(err.Error(), "no member found") {
// 如果没有找到成员,说明需要新建成员
NewUser := &User{
Phone: phone,
Email: email,
MemberName: target.Username,
RefID: refID,
}
err = PostFlashDuty("/member/invite", appKey, NewUser)
if err != nil {
logger.Errorf("failed to update user: %v", err)
}
return
}
if err != nil {
logger.Errorf("failed to update user: %v", err)
}
}