package repository import ( "errors" "strconv" "strings" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) type MoneyAccess struct { UserID uint All bool Scopes []model.UserMoneyScope } func (access MoneyAccess) Allows(appCode string, regionID int64) bool { if access.All { return true } appCode = appctx.Normalize(appCode) for _, scope := range access.Scopes { if appctx.Normalize(scope.AppCode) != appCode { continue } if scope.RegionID == 0 || RegionIDLooseEqual(scope.RegionID, regionID) { return true } } return false } // RegionIDLooseEqual 对齐 legacy 大区 ID 的浮点圆整误差:雪花/合成 ID 超出 JS 安全整数, // 浏览器写回的值是 JS Number 的"最短可往返十进制"(如 …690882 → …691000),与 // int64(float64(x)) 的回转值(…690880)都不相等,唯一可靠的等价层是 float64 本身: // 只要两个十进制落在同一个 float64 桶里就视为同一区域(相邻 ID 间距远大于桶宽,不会误碰)。 func RegionIDLooseEqual(left int64, right int64) bool { if left == right { return true } if left == 0 || right == 0 { return false } return float64(left) == float64(right) } func (access MoneyAccess) AppCodes() []string { if access.All { return nil } set := map[string]struct{}{} for _, scope := range access.Scopes { appCode := appctx.Normalize(scope.AppCode) if appCode != "" { set[appCode] = struct{}{} } } out := make([]string, 0, len(set)) for appCode := range set { out = append(out, appCode) } return out } func (s *Store) MoneyAccessForUser(userID uint) (MoneyAccess, error) { if userID == 0 { return MoneyAccess{All: true}, nil } var user model.User if err := s.db.Preload("Roles").First(&user, userID).Error; err != nil { return MoneyAccess{}, err } scopes, err := s.ListUserMoneyScopes(userID) if err != nil { return MoneyAccess{}, err } if len(scopes) == 0 && userHasRole(user, "platform-admin") { return MoneyAccess{UserID: userID, All: true}, nil } return MoneyAccess{UserID: userID, Scopes: scopes}, nil } func (s *Store) ListUserMoneyScopes(userID uint) ([]model.UserMoneyScope, error) { var scopes []model.UserMoneyScope err := s.db.Where("user_id = ?", userID).Order("app_code ASC, region_id ASC").Find(&scopes).Error return scopes, err } func (s *Store) ReplaceUserMoneyScopes(userID uint, scopes []model.UserMoneyScope) error { if userID == 0 { return errors.New("user id is required") } normalized, err := normalizeMoneyScopes(userID, scopes) if err != nil { return err } return s.db.Transaction(func(tx *gorm.DB) error { var user model.User if err := tx.First(&user, userID).Error; err != nil { return err } if err := tx.Where("user_id = ?", userID).Delete(&model.UserMoneyScope{}).Error; err != nil { return err } if len(normalized) == 0 { return nil } return tx.Create(&normalized).Error }) } func (s *Store) RecordTemporaryPaymentLinkOwner(owner model.TemporaryPaymentLinkOwner) error { owner.AppCode = appctx.Normalize(owner.AppCode) owner.OrderID = strings.TrimSpace(owner.OrderID) if owner.AppCode == "" || owner.OrderID == "" || owner.AdminUserID == 0 || owner.RegionID <= 0 { return errors.New("temporary payment link owner is incomplete") } nowMS := time.Now().UTC().UnixMilli() if owner.CreatedAtMS == 0 { owner.CreatedAtMS = nowMS } owner.UpdatedAtMS = nowMS return s.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "app_code"}, {Name: "order_id"}}, DoUpdates: clause.AssignmentColumns([]string{ "admin_user_id", "region_id", "updated_at_ms", }), }).Create(&owner).Error } func (s *Store) TemporaryPaymentLinkOwners(appCode string, orderIDs []string) (map[string]model.TemporaryPaymentLinkOwner, error) { appCode = appctx.Normalize(appCode) ids := uniqueStrings(orderIDs) out := make(map[string]model.TemporaryPaymentLinkOwner, len(ids)) if appCode == "" || len(ids) == 0 { return out, nil } var owners []model.TemporaryPaymentLinkOwner if err := s.db.Where("app_code = ? AND order_id IN ?", appCode, ids).Find(&owners).Error; err != nil { return nil, err } for _, owner := range owners { out[owner.OrderID] = owner } return out, nil } func (s *Store) FindUsersByIDs(ids []uint) (map[uint]model.User, error) { uniqueIDs := uniqueUints(ids) out := make(map[uint]model.User, len(uniqueIDs)) if len(uniqueIDs) == 0 { return out, nil } var users []model.User if err := s.db.Where("id IN ?", uniqueIDs).Find(&users).Error; err != nil { return nil, err } for _, user := range users { out[user.ID] = user } return out, nil } func normalizeMoneyScopes(userID uint, scopes []model.UserMoneyScope) ([]model.UserMoneyScope, error) { seen := map[string]struct{}{} out := make([]model.UserMoneyScope, 0, len(scopes)) for _, scope := range scopes { appCode := appctx.Normalize(scope.AppCode) if appCode == "" || scope.RegionID < 0 { return nil, errors.New("money scope app code and region id are required") } key := appCode + ":" + strconv.FormatInt(scope.RegionID, 10) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, model.UserMoneyScope{UserID: userID, AppCode: appCode, RegionID: scope.RegionID}) } return out, nil } func userHasRole(user model.User, code string) bool { for _, role := range user.Roles { if role.Code == code { return true } } return false } func uniqueStrings(values []string) []string { set := map[string]struct{}{} out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := set[value]; ok { continue } set[value] = struct{}{} out = append(out, value) } return out } func uniqueUints(values []uint) []uint { set := map[uint]struct{}{} out := make([]uint, 0, len(values)) for _, value := range values { if value == 0 { continue } if _, ok := set[value]; ok { continue } set[value] = struct{}{} out = append(out, value) } return out }