2026-05-18 19:22:25 +08:00

236 lines
7.1 KiB
Go

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
}