Files
olons_server/internal/portal/service.go
T

250 lines
7.8 KiB
Go

package portal
import (
"context"
"encoding/json"
"net/url"
"regexp"
"strings"
"git.118im.com/im_team/im_workbench/internal/accounts"
"git.118im.com/im_team/im_workbench/internal/store"
)
var (
positionPattern = regexp.MustCompile(`^[a-z0-9_-]{1,32}$`)
contentKeyPattern = regexp.MustCompile(`^[a-z0-9_-]{1,64}$`)
)
type Repository interface {
CurrentSiteConfig(context.Context, int64) (json.RawMessage, error)
ActiveBanners(context.Context, int64, string) ([]store.Banner, error)
PublishedContent(context.Context, int64, string) (*store.Content, error)
ActiveMemberLevels(context.Context, int64) ([]store.MemberLevel, error)
ActiveMemberNotices(context.Context, int64, int64, int) ([]store.MemberNotice, error)
MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error)
}
type Service struct {
repository Repository
}
type Features struct {
TaskEnabled bool `json:"task_enabled"`
RechargeEnabled bool `json:"recharge_enabled"`
WithdrawalEnabled bool `json:"withdrawal_enabled"`
ManualMallEnabled bool `json:"manual_mall_enabled"`
}
type Banner struct {
ID int64 `json:"id"`
ImageURL string `json:"image_url"`
AltText string `json:"alt_text"`
TargetType string `json:"target_type"`
TargetValue string `json:"target_value,omitempty"`
}
type Notice struct {
ID int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
SourceType string `json:"source_type"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
}
type Home struct {
Profile HomeProfile `json:"profile"`
Wallet accounts.ProfileWallet `json:"wallet"`
Features Features `json:"features"`
Banners []Banner `json:"banners"`
Notices []Notice `json:"notices"`
TaskSummary store.TaskSummary `json:"task_summary"`
}
type HomeProfile struct {
Username string `json:"username"`
AvatarURL string `json:"avatar_url"`
LevelName string `json:"level_name"`
}
type Content struct {
ContentKey string `json:"content_key"`
ContentType string `json:"content_type"`
Title string `json:"title"`
Summary string `json:"summary"`
BodyHTML string `json:"body_html"`
BodyText string `json:"body_text"`
CoverURL string `json:"cover_url"`
AssetIDs []int64 `json:"asset_ids"`
Version int `json:"version"`
PublishedAt string `json:"published_at"`
}
type MemberLevel struct {
ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
IconURL string `json:"icon_url"`
UpgradeAmount int64 `json:"upgrade_amount"`
GiftAmount int64 `json:"gift_amount"`
DailyOrderLimit int `json:"daily_order_limit"`
CommissionRate int `json:"commission_rate"`
DailyWithdrawLimit int `json:"daily_withdraw_limit"`
Rules json.RawMessage `json:"rules"`
Version int `json:"version"`
}
func New(repository Repository) *Service {
return &Service{repository: repository}
}
func (s *Service) Home(ctx context.Context, tenantID, memberID int64, profile *accounts.Profile) (*Home, error) {
config, err := s.repository.CurrentSiteConfig(ctx, tenantID)
if err != nil {
return nil, err
}
banners, err := s.Banners(ctx, tenantID, "home_top")
if err != nil {
return nil, err
}
noticeRows, err := s.repository.ActiveMemberNotices(ctx, tenantID, memberID, 5)
if err != nil {
return nil, err
}
summary, err := s.repository.MemberTaskSummary(ctx, tenantID, memberID)
if err != nil {
return nil, err
}
notices := make([]Notice, 0, len(noticeRows))
for _, row := range noticeRows {
notices = append(notices, Notice{ID: row.ID, Title: row.Title, Content: row.Content,
SourceType: noticeSourceName(row.SourceType), StartAt: row.StartAt, EndAt: row.EndAt})
}
return &Home{
Profile: HomeProfile{Username: profile.Username, AvatarURL: profile.AvatarURL, LevelName: profile.Level.Name},
Wallet: profile.Wallet, Features: parseFeatures(config), Banners: banners, Notices: notices,
TaskSummary: *summary,
}, nil
}
func (s *Service) Banners(ctx context.Context, tenantID int64, position string) ([]Banner, error) {
position = strings.ToLower(strings.TrimSpace(position))
if !positionPattern.MatchString(position) {
return nil, ErrPositionInvalid
}
rows, err := s.repository.ActiveBanners(ctx, tenantID, position)
if err != nil {
return nil, err
}
items := make([]Banner, 0, len(rows))
for _, row := range rows {
targetType, targetValue := bannerTarget(row.TargetType, row.TargetValue)
items = append(items, Banner{ID: row.ID, ImageURL: row.ImageURL, AltText: row.AltText,
TargetType: targetType, TargetValue: targetValue})
}
return items, nil
}
func (s *Service) Content(ctx context.Context, tenantID int64, key string) (*Content, error) {
key = strings.ToLower(strings.TrimSpace(key))
if !contentKeyPattern.MatchString(key) {
return nil, ErrContentKeyInvalid
}
row, err := s.repository.PublishedContent(ctx, tenantID, key)
if err != nil {
return nil, err
}
assetIDs := make([]int64, 0)
if row.AssetIDs.Valid && strings.TrimSpace(row.AssetIDs.String) != "" {
_ = json.Unmarshal([]byte(row.AssetIDs.String), &assetIDs)
}
return &Content{ContentKey: row.ContentKey, ContentType: row.ContentType, Title: row.Title,
Summary: row.Summary, BodyHTML: row.BodyHTML, BodyText: row.BodyText, CoverURL: row.CoverURL,
AssetIDs: assetIDs, Version: row.Version, PublishedAt: row.PublishedAt}, nil
}
func (s *Service) MemberLevels(ctx context.Context, tenantID int64) ([]MemberLevel, error) {
rows, err := s.repository.ActiveMemberLevels(ctx, tenantID)
if err != nil {
return nil, err
}
items := make([]MemberLevel, 0, len(rows))
for _, row := range rows {
rules := row.RuleJSON
if len(rules) == 0 {
rules = json.RawMessage(`{}`)
}
items = append(items, MemberLevel{ID: row.ID, Code: row.Code, Name: row.Name, IconURL: row.IconURL,
UpgradeAmount: row.UpgradeAmount, GiftAmount: row.GiftAmount, DailyOrderLimit: row.DailyOrderLimit,
CommissionRate: row.CommissionRate, DailyWithdrawLimit: row.DailyWithdrawLimit,
Rules: rules, Version: row.Version})
}
return items, nil
}
func parseFeatures(raw json.RawMessage) Features {
var config map[string]any
if json.Unmarshal(raw, &config) != nil {
return Features{}
}
return Features{
TaskEnabled: findBool(config, "task_enabled", "taskEnabled", "TASK"),
RechargeEnabled: findBool(config, "recharge_enabled", "rechargeEnabled", "RECHARGE"),
WithdrawalEnabled: findBool(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"),
ManualMallEnabled: findBool(config, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"),
}
}
func findBool(config map[string]any, keys ...string) bool {
for _, key := range keys {
if value, ok := config[key]; ok {
return boolValue(value)
}
}
if nested, ok := config["features"].(map[string]any); ok {
for _, key := range keys {
if value, found := nested[key]; found {
return boolValue(value)
}
}
}
return false
}
func boolValue(value any) bool {
switch typed := value.(type) {
case bool:
return typed
case float64:
return typed == 1
case string:
return typed == "1" || strings.EqualFold(typed, "true") || strings.EqualFold(typed, "on")
default:
return false
}
}
func bannerTarget(kind int, value string) (string, string) {
value = strings.TrimSpace(value)
switch kind {
case 2:
if strings.HasPrefix(value, "/") && !strings.HasPrefix(value, "//") {
return "internal_route", value
}
case 3:
parsed, err := url.Parse(value)
if err == nil && parsed.Scheme == "https" && parsed.Host != "" {
return "external_url", value
}
}
return "none", ""
}
func noticeSourceName(source int) string {
if source == 2 {
return "operational_simulation"
}
return "real"
}