mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/masterdata"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
"lunar-tear/server/internal/userdata"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var shopDiffTables = []string{
|
||||
"IUserShopItem",
|
||||
"IUserShopReplaceable",
|
||||
"IUserShopReplaceableLineup",
|
||||
"IUserGem",
|
||||
"IUserConsumableItem",
|
||||
"IUserMaterial",
|
||||
"IUserImportantItem",
|
||||
"IUserPremiumItem",
|
||||
"IUserStatus",
|
||||
"IUserCostume",
|
||||
"IUserCostumeActiveSkill",
|
||||
"IUserCharacter",
|
||||
"IUserWeapon",
|
||||
"IUserWeaponSkill",
|
||||
"IUserWeaponAbility",
|
||||
"IUserWeaponNote",
|
||||
"IUserWeaponStory",
|
||||
}
|
||||
|
||||
type ShopServiceServer struct {
|
||||
pb.UnimplementedShopServiceServer
|
||||
users store.UserRepository
|
||||
sessions store.SessionRepository
|
||||
catalog *masterdata.ShopCatalog
|
||||
granter *store.PossessionGranter
|
||||
}
|
||||
|
||||
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer {
|
||||
return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) {
|
||||
log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
for shopItemId, qty := range req.ShopItems {
|
||||
item, ok := s.catalog.Items[shopItemId]
|
||||
if !ok {
|
||||
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
|
||||
continue
|
||||
}
|
||||
|
||||
totalPrice := item.Price * qty
|
||||
if err := store.DeductPrice(user, item.PriceType, item.PriceId, totalPrice); err != nil {
|
||||
log.Printf("[ShopService] Buy: deduct failed shopItemId=%d: %v", shopItemId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, content := range s.catalog.Contents[shopItemId] {
|
||||
s.granter.GrantFull(user,
|
||||
model.PossessionType(content.PossessionType),
|
||||
content.PossessionId,
|
||||
content.Count*qty,
|
||||
nowMillis,
|
||||
)
|
||||
}
|
||||
|
||||
s.applyContentEffects(user, shopItemId, qty, nowMillis)
|
||||
|
||||
si := user.ShopItems[shopItemId]
|
||||
si.ShopItemId = shopItemId
|
||||
si.BoughtCount += qty
|
||||
si.LatestBoughtCountChangedDatetime = nowMillis
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[shopItemId] = si
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shop buy: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.BuyResponse{
|
||||
OverflowPossession: []*pb.Possession{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
||||
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 {
|
||||
for i, itemId := range s.catalog.ItemShopPool {
|
||||
slot := int32(i + 1)
|
||||
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
|
||||
SlotNumber: slot,
|
||||
ShopItemId: itemId,
|
||||
LatestVersion: nowMillis,
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.IsGemUsed {
|
||||
user.ShopReplaceable.LineupUpdateCount++
|
||||
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
|
||||
for _, itemId := range s.catalog.ItemShopPool {
|
||||
if si, ok := user.ShopItems[itemId]; ok {
|
||||
si.BoughtCount = 0
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[itemId] = si
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shop refresh: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.RefreshResponse{
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) GetCesaLimit(_ context.Context, _ *emptypb.Empty) (*pb.GetCesaLimitResponse, error) {
|
||||
log.Printf("[ShopService] GetCesaLimit")
|
||||
return &pb.GetCesaLimitResponse{
|
||||
CesaLimit: []*pb.CesaLimit{},
|
||||
DiffUserData: userdata.EmptyDiff(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *pb.CreatePurchaseTransactionRequest) (*pb.CreatePurchaseTransactionResponse, error) {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
|
||||
req.ShopId, req.ShopItemId, req.ProductId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
nowMillis := gametime.NowMillis()
|
||||
|
||||
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
item, ok := s.catalog.Items[req.ShopItemId]
|
||||
if !ok {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
|
||||
return
|
||||
}
|
||||
|
||||
if err := store.DeductPrice(user, item.PriceType, item.PriceId, item.Price); err != nil {
|
||||
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
|
||||
}
|
||||
|
||||
for _, content := range s.catalog.Contents[req.ShopItemId] {
|
||||
s.granter.GrantFull(user,
|
||||
model.PossessionType(content.PossessionType),
|
||||
content.PossessionId,
|
||||
content.Count,
|
||||
nowMillis,
|
||||
)
|
||||
}
|
||||
|
||||
s.applyContentEffects(user, req.ShopItemId, 1, nowMillis)
|
||||
|
||||
si := user.ShopItems[req.ShopItemId]
|
||||
si.ShopItemId = req.ShopItemId
|
||||
si.BoughtCount++
|
||||
if item.ShopItemLimitedStockId > 0 {
|
||||
if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
|
||||
si.BoughtCount = 0
|
||||
}
|
||||
}
|
||||
si.LatestBoughtCountChangedDatetime = nowMillis
|
||||
si.LatestVersion = nowMillis
|
||||
user.ShopItems[req.ShopItemId] = si
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create purchase transaction: %w", err)
|
||||
}
|
||||
|
||||
txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis)
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.CreatePurchaseTransactionResponse{
|
||||
PurchaseTransactionId: txId,
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context, req *pb.PurchaseGooglePlayStoreProductRequest) (*pb.PurchaseGooglePlayStoreProductResponse, error) {
|
||||
log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId)
|
||||
|
||||
userId := currentUserId(ctx, s.users, s.sessions)
|
||||
snapshot, err := s.users.SnapshotUser(userId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("purchase google play: %w", err)
|
||||
}
|
||||
|
||||
tables := userdata.FullClientTableMap(snapshot)
|
||||
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
||||
|
||||
return &pb.PurchaseGooglePlayStoreProductResponse{
|
||||
OverflowPossession: []*pb.Possession{},
|
||||
DiffUserData: diff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) {
|
||||
for _, effect := range s.catalog.Effects[shopItemId] {
|
||||
switch effect.EffectTargetType {
|
||||
case model.EffectTargetStaminaRecovery:
|
||||
maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level]
|
||||
millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level)
|
||||
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
|
||||
default:
|
||||
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 {
|
||||
switch effectValueType {
|
||||
case model.EffectValueFixed:
|
||||
return effectValue
|
||||
case model.EffectValuePermil:
|
||||
maxMillis := s.catalog.MaxStaminaMillis[userLevel]
|
||||
return effectValue * maxMillis / 1000
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user