230 lines
6.0 KiB
Go
230 lines
6.0 KiB
Go
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
|
||
}
|