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 }