2026-07-15 09:43:33 +08:00

155 lines
4.3 KiB
Go

package resource
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"strings"
)
type ResourceGroupItem struct {
GroupItemID int64
GroupID int64
ItemType string
ResourceID int64
Resource Resource
WalletAssetType string
WalletAssetAmount int64
Quantity int64
DurationMS int64
SortOrder int32
CreatedAtMS int64
UpdatedAtMS int64
// GiftConfigs is populated only for immutable group snapshots. omitempty preserves the canonical JSON
// of snapshots written before gift semantics became part of the wallet-owned reward promise.
GiftConfigs []GiftConfig `json:",omitempty"`
}
type ResourceGroup struct {
AppCode string
GroupID int64
GroupCode string
Name string
Status string
Description string
SortOrder int32
Items []ResourceGroupItem
CreatedByUserID int64
UpdatedByUserID int64
CreatedAtMS int64
UpdatedAtMS int64
}
// ResourceGroupSnapshot is an immutable, owner-controlled reward promise. The embedded group contains
// complete resource and wallet-asset facts needed both for deterministic grant execution and client display.
type ResourceGroupSnapshot struct {
AppCode string
SnapshotID string
PinKey string
SourceGroupID int64
VersionNo int64
SnapshotHash string
Group ResourceGroup
CreatedByUserID int64
CreatedAtMS int64
SourceGroupUpdatedAtMS int64
SourceContentHash string
}
type PinResourceGroupSnapshotCommand struct {
AppCode string
PinKey string
GroupID int64
OperatorUserID int64
ExpectedGroupUpdatedAtMS int64
ExpectedSourceContentHash string
RequiredRegionIDs []int64
RequiredAllRegions bool
}
// CanonicalResourceGroupJSON is the owner-side version identity used by both inspect and pin.
// It deliberately includes nested Resource bytes so an independently edited resource cannot reuse a stale pin.
func CanonicalResourceGroupJSON(group ResourceGroup) ([]byte, error) {
canonical := group
canonical.Items = append([]ResourceGroupItem(nil), group.Items...)
for index := range canonical.Items {
canonical.Items[index].Resource.UsageScopes = append([]string(nil), canonical.Items[index].Resource.UsageScopes...)
sort.Strings(canonical.Items[index].Resource.UsageScopes)
canonical.Items[index].GiftConfigs = canonicalGiftConfigs(canonical.Items[index].GiftConfigs)
}
sort.SliceStable(canonical.Items, func(left, right int) bool {
if canonical.Items[left].SortOrder != canonical.Items[right].SortOrder {
return canonical.Items[left].SortOrder < canonical.Items[right].SortOrder
}
return canonical.Items[left].GroupItemID < canonical.Items[right].GroupItemID
})
return json.Marshal(canonical)
}
func ResourceGroupContentHash(group ResourceGroup) (string, error) {
body, err := CanonicalResourceGroupJSON(group)
if err != nil {
return "", err
}
sum := sha256.Sum256(body)
return hex.EncodeToString(sum[:]), nil
}
type ResourceGroupItemInput struct {
ItemType string
ResourceID int64
WalletAssetType string
WalletAssetAmount int64
Quantity int64
DurationMS int64
SortOrder int32
}
type ListResourceGroupsQuery struct {
AppCode string
Status string
Keyword string
Page int32
PageSize int32
ActiveOnly bool
}
type ResourceGroupCommand struct {
AppCode string
GroupID int64
GroupCode string
Name string
Status string
Description string
SortOrder int32
Items []ResourceGroupItemInput
OperatorUserID int64
}
func ValidGrantStrategy(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case GrantStrategyWalletCredit, GrantStrategyNewEntitlement, GrantStrategyExtendExpiry, GrantStrategyIncreaseQuantity, GrantStrategySetActiveFlag:
return true
default:
return false
}
}
func NormalizeGroupItemType(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return GroupItemTypeResource
}
return value
}
func ValidGroupItemType(value string) bool {
switch NormalizeGroupItemType(value) {
case GroupItemTypeResource, GroupItemTypeWalletAsset:
return true
default:
return false
}
}