package activity import ( "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "context" "crypto/rand" "crypto/sha256" "errors" ) type ActivityCommitmentService struct { read *dao.Query write *dao.Query repo mysql.Repo } func NewActivityCommitmentService(read *dao.Query, write *dao.Query, repo mysql.Repo) *ActivityCommitmentService { return &ActivityCommitmentService{read: read, write: write, repo: repo} } func (s *ActivityCommitmentService) Generate(ctx context.Context, activityID int64) (int32, error) { act, err := s.read.Activities.WithContext(ctx).Where(s.read.Activities.ID.Eq(activityID)).First() if err != nil || act == nil { return 0, errors.New("activity not found") } var cur int32 _ = s.repo.GetDbR().Raw("SELECT IFNULL(commitment_state_version,0) FROM activities WHERE id=?", activityID).Scan(&cur) seed := make([]byte, 32) _, _ = rand.Read(seed) seedHash := sha256.Sum256(seed) // compute items_root by aggregating all issues' reward slots issueIDs := make([]int64, 0) issues, _ := s.read.ActivityIssues.WithContext(ctx).ReadDB().Where(s.read.ActivityIssues.ActivityID.Eq(activityID)).Find() for _, is := range issues { issueIDs = append(issueIDs, is.ID) } var itemsRoot []byte if len(issueIDs) > 0 { // fetch rewards per issue and build slots slots := make([]int64, 0) for _, iid := range issueIDs { rs, _ := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(iid)).Order( s.read.ActivityRewardSettings.IsBoss.Desc(), s.read.ActivityRewardSettings.Level.Asc(), s.read.ActivityRewardSettings.Sort.Asc(), ).Find() for _, r := range rs { for i := int64(0); i < r.OriginalQty; i++ { slots = append(slots, r.ID) } } } if len(slots) > 0 { // naive JSON encoding to compute root // Note: avoid importing encoding/json at top, compute hash over formatted bytes // but simpler: use []byte with little-endian concatenation h := sha256.New() for _, v := range slots { var buf [8]byte // big endian buf[0] = byte(v >> 56) buf[1] = byte(v >> 48) buf[2] = byte(v >> 40) buf[3] = byte(v >> 32) buf[4] = byte(v >> 24) buf[5] = byte(v >> 16) buf[6] = byte(v >> 8) buf[7] = byte(v) h.Write(buf[:]) } itemsRoot = h.Sum(nil) } } ver := cur + 1 _, err = s.write.Activities.WithContext(ctx).Where(s.write.Activities.ID.Eq(act.ID)).Updates(map[string]any{ "commitment_algo": "commit-v1", "commitment_seed_master": seed, "commitment_seed_hash": seedHash[:], "commitment_state_version": ver, "commitment_items_root": itemsRoot, }) if err != nil { return 0, err } return ver, nil } type ActivityCommitmentSummary struct { SeedVersion int32 Algo string HasSeed bool ItemsRoot []byte } func (s *ActivityCommitmentService) Summary(ctx context.Context, activityID int64) (ActivityCommitmentSummary, error) { var ver int32 var algo string var hasSeed bool // read version _ = s.repo.GetDbR().Raw("SELECT IFNULL(commitment_state_version,0) FROM activities WHERE id=?", activityID).Scan(&ver) _ = s.repo.GetDbR().Raw("SELECT commitment_algo FROM activities WHERE id=?", activityID).Scan(&algo) var seedLen *int _ = s.repo.GetDbR().Raw("SELECT LENGTH(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&seedLen) hasSeed = seedLen != nil && *seedLen > 0 return ActivityCommitmentSummary{SeedVersion: ver, Algo: algo, HasSeed: hasSeed, ItemsRoot: nil}, nil }