2026-05-08 18:55:47 +08:00

300 lines
7.0 KiB
Go

package apppopup
import (
"context"
"encoding/json"
"errors"
"net/http"
"sort"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/model"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// QueryEntryPopups 返回 APP 进场弹窗配置。服务端不做用户展示频控。
func (s *Service) QueryEntryPopups(ctx context.Context, user AuthUser, req EntryQueryRequest) (*EntryQueryResponse, error) {
if s == nil || s.db == nil {
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_unavailable", "app popup service is unavailable")
}
sysOrigin := normalizeSysOrigin(user.SysOrigin)
scene := normalizeScene(req.Scene)
configs, err := s.loadConfigs(ctx, sysOrigin, scene)
if err != nil {
return nil, err
}
now := s.now()
resp := &EntryQueryResponse{
RequestID: newRequestID(),
Queue: []string{},
Items: map[string]PopupItem{},
}
for _, cfg := range configs {
key := normalizePopupKey(cfg.PopupKey)
if key == "" {
continue
}
item := s.buildPopupItem(cfg, req, user.UserID, now)
resp.set(key, item)
if item.Value {
resp.Queue = append(resp.Queue, key)
}
}
sort.SliceStable(resp.Queue, func(i, j int) bool {
left := resp.Items[resp.Queue[i]]
right := resp.Items[resp.Queue[j]]
if left.Priority == right.Priority {
return resp.Queue[i] < resp.Queue[j]
}
return left.Priority > right.Priority
})
return resp, nil
}
func (s *Service) loadConfigs(ctx context.Context, sysOrigin, scene string) ([]cachedConfig, error) {
cacheKey := appPopupConfigCacheKey(sysOrigin, scene)
if s.cache != nil {
raw, err := s.cache.Get(ctx, cacheKey).Result()
if err == nil && strings.TrimSpace(raw) != "" {
var configs []cachedConfig
if jsonErr := json.Unmarshal([]byte(raw), &configs); jsonErr == nil {
return configs, nil
}
} else if err != nil && !errors.Is(err, redis.Nil) {
return nil, NewAppError(http.StatusServiceUnavailable, "app_popup_cache_failed", err.Error())
}
}
var rows []model.AppPopupConfig
err := s.db.WithContext(ctx).
Where("sys_origin = ? AND scene = ?", sysOrigin, scene).
Order("priority DESC").
Order("id ASC").
Find(&rows).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return []cachedConfig{}, nil
}
if err != nil {
return nil, err
}
configs := make([]cachedConfig, 0, len(rows))
for _, row := range rows {
configs = append(configs, toCachedConfig(row))
}
if s.cache != nil {
if encoded, err := json.Marshal(configs); err == nil {
_ = s.cache.Set(ctx, cacheKey, string(encoded), configCacheExpiration).Err()
}
}
return configs, nil
}
func appPopupConfigCacheKey(sysOrigin, scene string) string {
return "app_popup:configs:" + normalizeSysOrigin(sysOrigin) + ":" + normalizeScene(scene)
}
func (s *Service) buildPopupItem(cfg cachedConfig, req EntryQueryRequest, userID int64, now time.Time) PopupItem {
content := parsePopupContent(cfg.ContentJSON)
item := PopupItem{
Value: true,
Limit: normalizeLimitDays(cfg.LimitDays),
PopupID: cfg.ID,
Version: normalizeVersion(cfg.Version),
Priority: cfg.Priority,
Title: content.Title,
Image: content.Image,
JumpType: strings.TrimSpace(cfg.JumpType),
JumpURL: strings.TrimSpace(cfg.JumpURL),
Reason: "",
Content: content.Raw,
}
if !cfg.Enabled {
item.Value = false
item.Reason = reasonDisabled
return item
}
if cfg.StartTime != nil && !cfg.StartTime.IsZero() && now.Before(*cfg.StartTime) {
item.Value = false
item.Reason = reasonNotStarted
return item
}
if cfg.EndTime != nil && !cfg.EndTime.IsZero() && !now.Before(*cfg.EndTime) {
item.Value = false
item.Reason = reasonExpired
return item
}
if !matchPlatforms(cfg.PlatformsJSON, req.Platform) {
item.Value = false
item.Reason = reasonPlatform
return item
}
if !matchMinVersion(req.AppVersion, cfg.MinAppVersion) {
item.Value = false
item.Reason = reasonMinVersion
return item
}
if !matchMaxVersion(req.AppVersion, cfg.MaxAppVersion) {
item.Value = false
item.Reason = reasonMaxVersion
return item
}
if !matchUserScope(cfg.UserScopeType, cfg.UserScopeJSON, userID) {
item.Value = false
item.Reason = reasonUserScope
return item
}
return item
}
func normalizeLimitDays(limit int) int {
if limit < 0 {
return 0
}
return limit
}
func normalizeVersion(version int) int {
if version <= 0 {
return 1
}
return version
}
func matchPlatforms(raw, platform string) bool {
raw = strings.TrimSpace(raw)
platform = normalizePlatform(platform)
if raw == "" || raw == "null" || platform == "" {
return true
}
var platforms []string
if err := json.Unmarshal([]byte(raw), &platforms); err != nil {
return false
}
if len(platforms) == 0 {
return true
}
for _, item := range platforms {
if normalizePlatform(item) == platform {
return true
}
}
return false
}
func matchMinVersion(appVersion, minVersion string) bool {
appVersion = strings.TrimSpace(appVersion)
minVersion = strings.TrimSpace(minVersion)
if appVersion == "" || minVersion == "" {
return true
}
return compareVersion(appVersion, minVersion) >= 0
}
func matchMaxVersion(appVersion, maxVersion string) bool {
appVersion = strings.TrimSpace(appVersion)
maxVersion = strings.TrimSpace(maxVersion)
if appVersion == "" || maxVersion == "" {
return true
}
return compareVersion(appVersion, maxVersion) <= 0
}
func compareVersion(left, right string) int {
leftParts := parseVersion(left)
rightParts := parseVersion(right)
maxLen := len(leftParts)
if len(rightParts) > maxLen {
maxLen = len(rightParts)
}
for i := 0; i < maxLen; i++ {
var lv, rv int
if i < len(leftParts) {
lv = leftParts[i]
}
if i < len(rightParts) {
rv = rightParts[i]
}
if lv > rv {
return 1
}
if lv < rv {
return -1
}
}
return 0
}
func parseVersion(version string) []int {
version = strings.TrimSpace(version)
if version == "" {
return []int{0}
}
parts := strings.Split(version, ".")
result := make([]int, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
result = append(result, 0)
continue
}
digits := strings.Builder{}
for _, r := range part {
if r < '0' || r > '9' {
break
}
digits.WriteRune(r)
}
if digits.Len() == 0 {
result = append(result, 0)
continue
}
value, err := strconv.Atoi(digits.String())
if err != nil {
result = append(result, 0)
continue
}
result = append(result, value)
}
return result
}
func matchUserScope(scopeType, raw string, userID int64) bool {
switch strings.ToUpper(strings.TrimSpace(scopeType)) {
case "", "ALL":
return true
case "WHITELIST":
return userID > 0 && userScopeContains(raw, userID)
case "BLACKLIST":
return userID <= 0 || !userScopeContains(raw, userID)
default:
return false
}
}
func userScopeContains(raw string, userID int64) bool {
raw = strings.TrimSpace(raw)
if raw == "" || raw == "null" {
return false
}
var payload struct {
UserIDs []int64 `json:"userIds"`
}
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
return false
}
for _, item := range payload.UserIDs {
if item == userID {
return true
}
}
return false
}