diff --git a/cmd/api/main.go b/cmd/api/main.go index b3c1b4e..15e45d9 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -23,6 +23,7 @@ import ( "chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/managercenter" + "chatapp3-golang/internal/service/propsstore" "chatapp3-golang/internal/service/rechargeagency" "chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/regionimgroup" @@ -74,6 +75,7 @@ func main() { roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways) hostCenterService := hostcenter.NewService(app.Config, &app.Gateways) managerCenterService := managercenter.NewService(app.Repository.DB, &app.Gateways) + propsStoreService := propsstore.NewService(app.Repository.DB, app.Repository.Redis) baishunService.SetTaskEventReporter(taskCenterService) gameOpenService.SetTaskEventReporter(taskCenterService) @@ -113,6 +115,7 @@ func main() { GameProviders: gameProviders, LuckyGift: luckyGiftService, ManagerCenter: managerCenterService, + PropsStore: propsStoreService, HostCenter: hostCenterService, RechargeAgency: rechargeAgencyService, RechargeReward: rechargeRewardService, diff --git a/internal/model/props_models.go b/internal/model/props_models.go new file mode 100644 index 0000000..133cf1e --- /dev/null +++ b/internal/model/props_models.go @@ -0,0 +1,25 @@ +package model + +import "time" + +// PropsCommodityStore stores one props store commodity row. +type PropsCommodityStore struct { + ID int64 `gorm:"column:id;primaryKey"` + SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_props_store_source,priority:1"` + SourceID int64 `gorm:"column:source_id;index:idx_props_store_source,priority:2"` + PropsType string `gorm:"column:props_type;size:64"` + CurrencyTypes string `gorm:"column:currency_types;size:128"` + ValidDays string `gorm:"column:valid_days;size:128"` + Discount *float64 `gorm:"column:discount;type:decimal(10,4)"` + ShelfStatus *bool `gorm:"column:shelf_status"` + Sort int `gorm:"column:sort"` + Label string `gorm:"column:label;size:255"` + Del *bool `gorm:"column:is_del"` + CreateTime *time.Time `gorm:"column:create_time"` + UpdateTime *time.Time `gorm:"column:update_time"` + CreateUser *int64 `gorm:"column:create_user"` + UpdateUser *int64 `gorm:"column:update_user"` +} + +// TableName returns the props store commodity table name. +func (PropsCommodityStore) TableName() string { return "props_commodity_store" } diff --git a/internal/router/props_store_routes.go b/internal/router/props_store_routes.go new file mode 100644 index 0000000..3ee4f18 --- /dev/null +++ b/internal/router/props_store_routes.go @@ -0,0 +1,45 @@ +package router + +import ( + "net/http" + + "chatapp3-golang/internal/service/propsstore" + + "github.com/gin-gonic/gin" +) + +// registerPropsStoreRoutes registers admin props store routes. +func registerPropsStoreRoutes(engine *gin.Engine, javaClient authGateway, service *propsstore.Service) { + if service == nil { + return + } + + adminGroup := engine.Group("/props/store") + adminGroup.Use(consoleAuthMiddleware(javaClient)) + adminGroup.POST("/source-map", func(c *gin.Context) { + var req propsstore.MapBySourceIDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeError(c, propsstore.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) + return + } + resp, err := service.MapBySourceIDs(c.Request.Context(), req) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) + adminGroup.POST("/add-or-update", func(c *gin.Context) { + var req propsstore.SaveCommodityRequest + if err := c.ShouldBindJSON(&req); err != nil { + writeError(c, propsstore.NewAppError(http.StatusBadRequest, "bad_request", err.Error())) + return + } + resp, err := service.SaveCommodity(c.Request.Context(), req) + if err != nil { + writeError(c, err) + return + } + writeOK(c, resp) + }) +} diff --git a/internal/router/props_store_routes_test.go b/internal/router/props_store_routes_test.go new file mode 100644 index 0000000..67cff43 --- /dev/null +++ b/internal/router/props_store_routes_test.go @@ -0,0 +1,95 @@ +package router + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "chatapp3-golang/internal/config" + "chatapp3-golang/internal/integration" + "chatapp3-golang/internal/model" + "chatapp3-golang/internal/repo" + "chatapp3-golang/internal/service/propsstore" + + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestPropsStoreAdminRoutesSaveAndMap(t *testing.T) { + gin.SetMode(gin.TestMode) + db := newRouterPropsStoreDB(t) + service := propsstore.NewService(db, nil) + engine := NewRouter(config.Config{}, &repo.Repository{}, propsStoreAuthStub{}, Services{ + PropsStore: service, + }) + + saveBody := []byte(`{ + "sysOrigin": "LIKEI", + "sourceId": "100", + "propsType": "AVATAR_FRAME", + "currencyTypes": "GOLD", + "validDays": "7", + "discount": 0, + "shelfStatus": true, + "sort": 0, + "label": "" + }`) + saveReq := httptest.NewRequest(http.MethodPost, "/props/store/add-or-update", bytes.NewReader(saveBody)) + saveReq.Header.Set("Authorization", "Bearer test-token") + saveReq.Header.Set("Content-Type", "application/json") + saveResp := httptest.NewRecorder() + engine.ServeHTTP(saveResp, saveReq) + if saveResp.Code != http.StatusOK { + t.Fatalf("save status = %d, body = %s", saveResp.Code, saveResp.Body.String()) + } + + mapReq := httptest.NewRequest(http.MethodPost, "/props/store/source-map", bytes.NewReader([]byte(`{ + "sysOrigin": "LIKEI", + "sourceIds": ["100"] + }`))) + mapReq.Header.Set("Authorization", "Bearer test-token") + mapReq.Header.Set("Content-Type", "application/json") + mapResp := httptest.NewRecorder() + engine.ServeHTTP(mapResp, mapReq) + if mapResp.Code != http.StatusOK { + t.Fatalf("map status = %d, body = %s", mapResp.Code, mapResp.Body.String()) + } + + var decoded struct { + ErrorCode int `json:"errorCode"` + Body map[string]propsstore.CommodityView `json:"body"` + } + if err := json.Unmarshal(mapResp.Body.Bytes(), &decoded); err != nil { + t.Fatalf("decode response: %v", err) + } + item, ok := decoded.Body["100"] + if decoded.ErrorCode != 0 || !ok || item.ShelfStatus == nil || !*item.ShelfStatus { + t.Fatalf("unexpected source-map body: %#v", decoded) + } +} + +type propsStoreAuthStub struct{} + +func (propsStoreAuthStub) AuthenticateToken(context.Context, string) (integration.UserCredential, error) { + return integration.UserCredential{}, nil +} + +func (propsStoreAuthStub) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) { + return integration.ConsoleAccount{LoginName: "admin"}, nil +} + +func newRouterPropsStoreDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.PropsCommodityStore{}); err != nil { + t.Fatalf("auto migrate: %v", err) + } + return db +} diff --git a/internal/router/router.go b/internal/router/router.go index 0c6c0c7..12ff108 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -17,6 +17,7 @@ import ( "chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/managercenter" + "chatapp3-golang/internal/service/propsstore" "chatapp3-golang/internal/service/rechargeagency" "chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/regionimgroup" @@ -47,6 +48,7 @@ type Services struct { GameProviders *gameprovider.Registry LuckyGift *luckygift.LuckyGiftService ManagerCenter *managercenter.Service + PropsStore *propsstore.Service HostCenter *hostcenter.Service RechargeAgency *rechargeagency.Service RechargeReward *rechargereward.Service @@ -100,6 +102,7 @@ func NewRouter( registerLingxianRoutes(engine, javaClient, services.Lingxian) registerLuckyGiftRoutes(engine, cfg, services.LuckyGift) registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter) + registerPropsStoreRoutes(engine, javaClient, services.PropsStore) return engine } diff --git a/internal/service/propsstore/store.go b/internal/service/propsstore/store.go new file mode 100644 index 0000000..7a9e342 --- /dev/null +++ b/internal/service/propsstore/store.go @@ -0,0 +1,235 @@ +package propsstore + +import ( + "context" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "chatapp3-golang/internal/model" + "chatapp3-golang/internal/utils" + + "gorm.io/gorm" +) + +// MapBySourceIDs returns active store commodities keyed by source id. +func (s *Service) MapBySourceIDs(ctx context.Context, req MapBySourceIDsRequest) (map[string]CommodityView, error) { + if s == nil || s.db == nil { + return nil, NewAppError(http.StatusServiceUnavailable, "props_store_unavailable", "props store service is unavailable") + } + sysOrigin := normalizeSysOrigin(req.SysOrigin) + if sysOrigin == "" { + return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required") + } + sourceIDs := uniquePositiveSourceIDs(req.SourceIDs) + if len(sourceIDs) == 0 { + return map[string]CommodityView{}, nil + } + + var rows []model.PropsCommodityStore + if err := s.db.WithContext(ctx). + Model(&model.PropsCommodityStore{}). + Where("sys_origin = ? AND source_id IN ?", sysOrigin, sourceIDs). + Where("(is_del = ? OR is_del IS NULL)", false). + Order("update_time DESC"). + Find(&rows).Error; err != nil { + return nil, err + } + + result := make(map[string]CommodityView, len(rows)) + for _, row := range rows { + key := strconv.FormatInt(row.SourceID, 10) + if _, exists := result[key]; exists { + continue + } + result[key] = commodityViewFromModel(row) + } + return result, nil +} + +// SaveCommodity creates or updates one store commodity by sysOrigin and sourceId. +func (s *Service) SaveCommodity(ctx context.Context, req SaveCommodityRequest) (*CommodityView, error) { + if s == nil || s.db == nil { + return nil, NewAppError(http.StatusServiceUnavailable, "props_store_unavailable", "props store service is unavailable") + } + if err := validateSaveCommodityRequest(req); err != nil { + return nil, err + } + + sysOrigin := normalizeSysOrigin(req.SysOrigin) + sourceID := req.SourceID.Int64() + propsType := strings.ToUpper(strings.TrimSpace(req.PropsType)) + discount := 0.0 + if req.Discount != nil { + discount = *req.Discount + } + del := false + now := s.now() + var savedID int64 + var previousPropsType string + + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var existing model.PropsCommodityStore + findErr := tx.Where("sys_origin = ? AND source_id = ?", sysOrigin, sourceID). + Order("update_time DESC"). + First(&existing).Error + if findErr != nil && !errors.Is(findErr, gorm.ErrRecordNotFound) { + return findErr + } + + if findErr == nil { + previousPropsType = existing.PropsType + savedID = existing.ID + updates := map[string]any{ + "sys_origin": sysOrigin, + "source_id": sourceID, + "props_type": propsType, + "currency_types": strings.ToUpper(strings.TrimSpace(req.CurrencyTypes)), + "valid_days": strings.TrimSpace(req.ValidDays), + "discount": discount, + "shelf_status": *req.ShelfStatus, + "sort": req.Sort, + "label": strings.TrimSpace(req.Label), + "is_del": del, + "update_time": now, + } + return tx.Model(&model.PropsCommodityStore{}). + Where("id = ?", existing.ID). + Updates(updates).Error + } + + id, idErr := utils.NextID() + if idErr != nil { + return idErr + } + savedID = id + row := model.PropsCommodityStore{ + ID: id, + SysOrigin: sysOrigin, + SourceID: sourceID, + PropsType: propsType, + CurrencyTypes: strings.ToUpper(strings.TrimSpace(req.CurrencyTypes)), + ValidDays: strings.TrimSpace(req.ValidDays), + Discount: &discount, + ShelfStatus: req.ShelfStatus, + Sort: req.Sort, + Label: strings.TrimSpace(req.Label), + Del: &del, + CreateTime: &now, + UpdateTime: &now, + } + return tx.Create(&row).Error + }) + if err != nil { + return nil, err + } + + s.evictStoreCache(ctx, sysOrigin, propsType) + if strings.TrimSpace(previousPropsType) != "" && !strings.EqualFold(previousPropsType, propsType) { + s.evictStoreCache(ctx, sysOrigin, previousPropsType) + } + + var saved model.PropsCommodityStore + if err := s.db.WithContext(ctx).Where("id = ?", savedID).First(&saved).Error; err != nil { + return nil, err + } + view := commodityViewFromModel(saved) + return &view, nil +} + +func validateSaveCommodityRequest(req SaveCommodityRequest) error { + if normalizeSysOrigin(req.SysOrigin) == "" { + return NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required") + } + if req.SourceID.Int64() <= 0 { + return NewAppError(http.StatusBadRequest, "source_id_required", "sourceId is required") + } + if strings.TrimSpace(req.PropsType) == "" { + return NewAppError(http.StatusBadRequest, "props_type_required", "propsType is required") + } + if strings.TrimSpace(req.CurrencyTypes) == "" { + return NewAppError(http.StatusBadRequest, "currency_types_required", "currencyTypes is required") + } + if strings.TrimSpace(req.ValidDays) == "" { + return NewAppError(http.StatusBadRequest, "valid_days_required", "validDays is required") + } + if req.ShelfStatus == nil { + return NewAppError(http.StatusBadRequest, "shelf_status_required", "shelfStatus is required") + } + if req.Discount != nil && (*req.Discount < 0 || *req.Discount > 1) { + return NewAppError(http.StatusBadRequest, "invalid_discount", "discount must be between 0 and 1") + } + return nil +} + +func (s *Service) evictStoreCache(ctx context.Context, sysOrigin string, propsType string) { + if s == nil || s.cache == nil { + return + } + propsType = strings.ToUpper(strings.TrimSpace(propsType)) + if sysOrigin == "" || propsType == "" { + return + } + keys := []string{ + propsStoreCacheKey(sysOrigin, propsType, "GOLD"), + propsStoreCacheKey(sysOrigin, propsType, "DIAMOND"), + propsStoreCacheKey(sysOrigin, propsType, "FREE"), + } + _ = s.cache.Del(ctx, keys...).Err() +} + +func propsStoreCacheKey(sysOrigin string, propsType string, currencyType string) string { + return fmt.Sprintf("%s::%s_%s_%s", propsStoreCacheName, sysOrigin, propsType, currencyType) +} + +func commodityViewFromModel(row model.PropsCommodityStore) CommodityView { + view := CommodityView{ + ID: strconv.FormatInt(row.ID, 10), + SysOrigin: row.SysOrigin, + SourceID: strconv.FormatInt(row.SourceID, 10), + PropsType: row.PropsType, + CurrencyTypes: row.CurrencyTypes, + ValidDays: row.ValidDays, + Discount: row.Discount, + ShelfStatus: row.ShelfStatus, + Sort: row.Sort, + Label: row.Label, + Del: row.Del, + } + if row.CreateTime != nil && !row.CreateTime.IsZero() { + view.CreateTime = row.CreateTime.Format(timeLayout) + } + if row.UpdateTime != nil && !row.UpdateTime.IsZero() { + view.UpdateTime = row.UpdateTime.Format(timeLayout) + } + if row.CreateUser != nil { + view.CreateUser = strconv.FormatInt(*row.CreateUser, 10) + } + if row.UpdateUser != nil { + view.UpdateUser = strconv.FormatInt(*row.UpdateUser, 10) + } + return view +} + +func normalizeSysOrigin(sysOrigin string) string { + return strings.ToUpper(strings.TrimSpace(sysOrigin)) +} + +func uniquePositiveSourceIDs(values []flexibleInt64) []int64 { + seen := make(map[int64]struct{}, len(values)) + result := make([]int64, 0, len(values)) + for _, value := range values { + id := value.Int64() + if id <= 0 { + continue + } + if _, exists := seen[id]; exists { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result +} diff --git a/internal/service/propsstore/store_test.go b/internal/service/propsstore/store_test.go new file mode 100644 index 0000000..df3fb69 --- /dev/null +++ b/internal/service/propsstore/store_test.go @@ -0,0 +1,165 @@ +package propsstore + +import ( + "context" + "testing" + "time" + + "chatapp3-golang/internal/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestMapBySourceIDsReturnsLatestActiveCommodity(t *testing.T) { + db := newPropsStoreTestDB(t) + now := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC) + older := now.Add(-time.Hour) + delFalse := false + delTrue := true + shelfFalse := false + shelfTrue := true + discount := 0.0 + + mustCreatePropsStore(t, db, model.PropsCommodityStore{ + ID: 1, + SysOrigin: "LIKEI", + SourceID: 100, + PropsType: "AVATAR_FRAME", + CurrencyTypes: "FREE", + ValidDays: "1", + Discount: &discount, + ShelfStatus: &shelfFalse, + Sort: 1, + Del: nil, + UpdateTime: &older, + }) + mustCreatePropsStore(t, db, model.PropsCommodityStore{ + ID: 2, + SysOrigin: "LIKEI", + SourceID: 100, + PropsType: "AVATAR_FRAME", + CurrencyTypes: "GOLD", + ValidDays: "7", + Discount: &discount, + ShelfStatus: &shelfTrue, + Sort: 2, + Del: &delFalse, + UpdateTime: &now, + }) + mustCreatePropsStore(t, db, model.PropsCommodityStore{ + ID: 3, + SysOrigin: "LIKEI", + SourceID: 200, + PropsType: "AVATAR_FRAME", + CurrencyTypes: "GOLD", + ValidDays: "7", + Discount: &discount, + ShelfStatus: &shelfTrue, + Sort: 1, + Del: &delTrue, + UpdateTime: &now, + }) + + service := NewService(db, nil) + got, err := service.MapBySourceIDs(context.Background(), MapBySourceIDsRequest{ + SysOrigin: "likei", + SourceIDs: []flexibleInt64{ + flexibleInt64(100), + flexibleInt64(200), + }, + }) + if err != nil { + t.Fatalf("MapBySourceIDs() error = %v", err) + } + if len(got) != 1 { + t.Fatalf("MapBySourceIDs() len = %d, want 1", len(got)) + } + item, ok := got["100"] + if !ok { + t.Fatalf("MapBySourceIDs() missing source 100: %#v", got) + } + if item.ID != "2" || item.CurrencyTypes != "GOLD" || item.ShelfStatus == nil || !*item.ShelfStatus { + t.Fatalf("MapBySourceIDs() item = %#v", item) + } +} + +func TestSaveCommodityUpdatesExistingAndRestoresDelFalse(t *testing.T) { + db := newPropsStoreTestDB(t) + now := time.Date(2026, 5, 18, 12, 0, 0, 0, time.UTC) + delTrue := true + shelfFalse := false + discount := 0.0 + mustCreatePropsStore(t, db, model.PropsCommodityStore{ + ID: 1, + SysOrigin: "LIKEI", + SourceID: 100, + PropsType: "AVATAR_FRAME", + CurrencyTypes: "FREE", + ValidDays: "1", + Discount: &discount, + ShelfStatus: &shelfFalse, + Sort: 1, + Label: "old", + Del: &delTrue, + UpdateTime: &now, + }) + + shelfTrue := true + service := NewService(db, nil) + service.now = func() time.Time { return now.Add(time.Hour) } + got, err := service.SaveCommodity(context.Background(), SaveCommodityRequest{ + SysOrigin: "LIKEI", + SourceID: flexibleInt64(100), + PropsType: "AVATAR_FRAME", + CurrencyTypes: "GOLD", + ValidDays: "7", + Discount: &discount, + ShelfStatus: &shelfTrue, + Sort: 0, + Label: "", + }) + if err != nil { + t.Fatalf("SaveCommodity() error = %v", err) + } + if got.ID != "1" || got.CurrencyTypes != "GOLD" || got.Label != "" { + t.Fatalf("SaveCommodity() = %#v", got) + } + + var count int64 + if err := db.Model(&model.PropsCommodityStore{}).Count(&count).Error; err != nil { + t.Fatalf("Count() error = %v", err) + } + if count != 1 { + t.Fatalf("row count = %d, want 1", count) + } + var row model.PropsCommodityStore + if err := db.First(&row, "id = ?", int64(1)).Error; err != nil { + t.Fatalf("First() error = %v", err) + } + if row.Del == nil || *row.Del { + t.Fatalf("row.Del = %v, want false", row.Del) + } + if row.ShelfStatus == nil || !*row.ShelfStatus { + t.Fatalf("row.ShelfStatus = %v, want true", row.ShelfStatus) + } +} + +func newPropsStoreTestDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&model.PropsCommodityStore{}); err != nil { + t.Fatalf("auto migrate: %v", err) + } + return db +} + +func mustCreatePropsStore(t *testing.T, db *gorm.DB, row model.PropsCommodityStore) { + t.Helper() + if err := db.Create(&row).Error; err != nil { + t.Fatalf("create props store: %v", err) + } +} diff --git a/internal/service/propsstore/types.go b/internal/service/propsstore/types.go new file mode 100644 index 0000000..2e7dbcd --- /dev/null +++ b/internal/service/propsstore/types.go @@ -0,0 +1,116 @@ +package propsstore + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "chatapp3-golang/internal/common" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +const ( + propsStoreCacheName = "PROPS_STORE" + timeLayout = "2006-01-02 15:04:05" +) + +type AppError = common.AppError + +var NewAppError = common.NewAppError + +type dbHandle interface { + WithContext(ctx context.Context) *gorm.DB +} + +// Service handles admin props store commodity operations. +type Service struct { + db dbHandle + cache redis.Cmdable + now func() time.Time +} + +// NewService creates a props store service. +func NewService(db dbHandle, cache redis.Cmdable) *Service { + return &Service{ + db: db, + cache: cache, + now: time.Now, + } +} + +// MapBySourceIDsRequest requests store commodities by props source ids. +type MapBySourceIDsRequest struct { + SysOrigin string `json:"sysOrigin"` + SourceIDs []flexibleInt64 `json:"sourceIds"` +} + +// SaveCommodityRequest creates or updates one store commodity. +type SaveCommodityRequest struct { + ID flexibleInt64 `json:"id"` + SysOrigin string `json:"sysOrigin"` + SourceID flexibleInt64 `json:"sourceId"` + PropsType string `json:"propsType"` + CurrencyTypes string `json:"currencyTypes"` + ValidDays string `json:"validDays"` + Discount *float64 `json:"discount"` + ShelfStatus *bool `json:"shelfStatus"` + Sort int `json:"sort"` + Label string `json:"label"` + Del *bool `json:"del"` +} + +// CommodityView is the admin-facing props store commodity DTO. +type CommodityView struct { + ID string `json:"id,omitempty"` + SysOrigin string `json:"sysOrigin"` + SourceID string `json:"sourceId"` + PropsType string `json:"propsType"` + CurrencyTypes string `json:"currencyTypes"` + ValidDays string `json:"validDays"` + Discount *float64 `json:"discount"` + ShelfStatus *bool `json:"shelfStatus"` + Sort int `json:"sort"` + Label string `json:"label"` + Del *bool `json:"del"` + CreateTime string `json:"createTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + CreateUser string `json:"createUser,omitempty"` + UpdateUser string `json:"updateUser,omitempty"` +} + +type flexibleInt64 int64 + +func (v *flexibleInt64) UnmarshalJSON(data []byte) error { + raw := strings.TrimSpace(string(data)) + switch raw { + case "", "null", `""`: + *v = 0 + return nil + } + if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) { + unquoted, err := strconv.Unquote(raw) + if err != nil { + return err + } + raw = strings.TrimSpace(unquoted) + } + parsed, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return fmt.Errorf("invalid int64 value: %w", err) + } + *v = flexibleInt64(parsed) + return nil +} + +func (v flexibleInt64) Int64() int64 { + return int64(v) +} + +func (v flexibleInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(strconv.FormatInt(int64(v), 10)) +}