Add props store admin go routes
This commit is contained in:
parent
c0d58525fe
commit
8c53a64416
@ -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,
|
||||||
|
|||||||
25
internal/model/props_models.go
Normal file
25
internal/model/props_models.go
Normal 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" }
|
||||||
45
internal/router/props_store_routes.go
Normal file
45
internal/router/props_store_routes.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
95
internal/router/props_store_routes_test.go
Normal file
95
internal/router/props_store_routes_test.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
235
internal/service/propsstore/store.go
Normal file
235
internal/service/propsstore/store.go
Normal 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
|
||||||
|
}
|
||||||
165
internal/service/propsstore/store_test.go
Normal file
165
internal/service/propsstore/store_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
116
internal/service/propsstore/types.go
Normal file
116
internal/service/propsstore/types.go
Normal 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))
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user