hyapp-server/server/admin/internal/repository/money_scope_repository.go
2026-07-03 22:04:39 +08:00

230 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}