Add props store admin go routes

This commit is contained in:
hy001 2026-05-18 19:22:25 +08:00
parent c0d58525fe
commit 8c53a64416
8 changed files with 687 additions and 0 deletions

View File

@ -23,6 +23,7 @@ import (
"chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/lingxian"
"chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/luckygift"
"chatapp3-golang/internal/service/managercenter" "chatapp3-golang/internal/service/managercenter"
"chatapp3-golang/internal/service/propsstore"
"chatapp3-golang/internal/service/rechargeagency" "chatapp3-golang/internal/service/rechargeagency"
"chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/rechargereward"
"chatapp3-golang/internal/service/regionimgroup" "chatapp3-golang/internal/service/regionimgroup"
@ -74,6 +75,7 @@ func main() {
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways) roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways) hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
managerCenterService := managercenter.NewService(app.Repository.DB, &app.Gateways) managerCenterService := managercenter.NewService(app.Repository.DB, &app.Gateways)
propsStoreService := propsstore.NewService(app.Repository.DB, app.Repository.Redis)
baishunService.SetTaskEventReporter(taskCenterService) baishunService.SetTaskEventReporter(taskCenterService)
gameOpenService.SetTaskEventReporter(taskCenterService) gameOpenService.SetTaskEventReporter(taskCenterService)
@ -113,6 +115,7 @@ func main() {
GameProviders: gameProviders, GameProviders: gameProviders,
LuckyGift: luckyGiftService, LuckyGift: luckyGiftService,
ManagerCenter: managerCenterService, ManagerCenter: managerCenterService,
PropsStore: propsStoreService,
HostCenter: hostCenterService, HostCenter: hostCenterService,
RechargeAgency: rechargeAgencyService, RechargeAgency: rechargeAgencyService,
RechargeReward: rechargeRewardService, RechargeReward: rechargeRewardService,

View File

@ -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" }

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -17,6 +17,7 @@ import (
"chatapp3-golang/internal/service/lingxian" "chatapp3-golang/internal/service/lingxian"
"chatapp3-golang/internal/service/luckygift" "chatapp3-golang/internal/service/luckygift"
"chatapp3-golang/internal/service/managercenter" "chatapp3-golang/internal/service/managercenter"
"chatapp3-golang/internal/service/propsstore"
"chatapp3-golang/internal/service/rechargeagency" "chatapp3-golang/internal/service/rechargeagency"
"chatapp3-golang/internal/service/rechargereward" "chatapp3-golang/internal/service/rechargereward"
"chatapp3-golang/internal/service/regionimgroup" "chatapp3-golang/internal/service/regionimgroup"
@ -47,6 +48,7 @@ type Services struct {
GameProviders *gameprovider.Registry GameProviders *gameprovider.Registry
LuckyGift *luckygift.LuckyGiftService LuckyGift *luckygift.LuckyGiftService
ManagerCenter *managercenter.Service ManagerCenter *managercenter.Service
PropsStore *propsstore.Service
HostCenter *hostcenter.Service HostCenter *hostcenter.Service
RechargeAgency *rechargeagency.Service RechargeAgency *rechargeagency.Service
RechargeReward *rechargereward.Service RechargeReward *rechargereward.Service
@ -100,6 +102,7 @@ func NewRouter(
registerLingxianRoutes(engine, javaClient, services.Lingxian) registerLingxianRoutes(engine, javaClient, services.Lingxian)
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift) registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter) registerManagerCenterRoutes(engine, javaClient, services.ManagerCenter)
registerPropsStoreRoutes(engine, javaClient, services.PropsStore)
return engine return engine
} }

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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))
}