package usecase import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/placement" settingdomain "haixun-backend/internal/model/setting/domain/usecase" ) const ( settingScopeUser = "user" keyResearch = "placement.research" ) type Settings struct { BraveAPIKey string BraveAPIKeyConfigured bool BraveCountry string BraveSearchLang string ExpandStrategy string } type SettingsPatch struct { BraveAPIKey *string BraveCountry *string BraveSearchLang *string ExpandStrategy *string } type UseCase interface { Get(ctx context.Context, tenantID, ownerUID string) (*Settings, error) Update(ctx context.Context, tenantID, ownerUID string, patch SettingsPatch) (*Settings, error) ResearchSettings(ctx context.Context, tenantID, ownerUID string) (placement.ResearchSettings, error) } type placementUseCase struct { settings settingdomain.UseCase } func NewUseCase(settings settingdomain.UseCase) UseCase { return &placementUseCase{settings: settings} } func (u *placementUseCase) Get(ctx context.Context, tenantID, ownerUID string) (*Settings, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } stored, err := u.load(ctx, ownerUID) if err != nil { return nil, err } return toPublic(stored), nil } func (u *placementUseCase) Update(ctx context.Context, tenantID, ownerUID string, patch SettingsPatch) (*Settings, error) { if err := requireActor(tenantID, ownerUID); err != nil { return nil, err } current, err := u.load(ctx, ownerUID) if err != nil { return nil, err } next := applyPatch(current, patch) if err := u.save(ctx, ownerUID, next); err != nil { return nil, err } return toPublic(next), nil } func (u *placementUseCase) ResearchSettings(ctx context.Context, tenantID, ownerUID string) (placement.ResearchSettings, error) { if err := requireActor(tenantID, ownerUID); err != nil { return placement.ResearchSettings{}, err } stored, err := u.load(ctx, ownerUID) if err != nil { return placement.ResearchSettings{}, err } return placement.ResearchSettings{ BraveAPIKey: stored.BraveAPIKey, BraveCountry: stored.BraveCountry, BraveSearchLang: stored.BraveSearchLang, ExpandStrategy: stored.ExpandStrategy, }, nil } func (u *placementUseCase) load(ctx context.Context, ownerUID string) (storedSettings, error) { defaults := defaultSettings() setting, err := u.settings.Get(ctx, settingScopeUser, ownerUID, keyResearch) if err != nil { if isSettingNotFound(err) { return defaults, nil } return defaults, err } return mergeSettings(defaults, setting.Value), nil } func (u *placementUseCase) save(ctx context.Context, ownerUID string, value storedSettings) error { _, err := u.settings.Upsert(ctx, settingdomain.UpsertRequest{ Scope: settingScopeUser, ScopeID: ownerUID, Key: keyResearch, Value: value.toMap(), }) return err } type storedSettings struct { BraveAPIKey string BraveCountry string BraveSearchLang string ExpandStrategy string } func defaultSettings() storedSettings { return storedSettings{ BraveCountry: "tw", BraveSearchLang: "zh-hant", ExpandStrategy: string(libkg.ExpandStrategyHybrid), } } func mergeSettings(defaults storedSettings, raw map[string]interface{}) storedSettings { if raw == nil { return defaults } if v, ok := raw["brave_api_key"].(string); ok { defaults.BraveAPIKey = strings.TrimSpace(v) } if v, ok := raw["brave_country"].(string); ok && strings.TrimSpace(v) != "" { defaults.BraveCountry = strings.TrimSpace(v) } if v, ok := raw["brave_search_lang"].(string); ok && strings.TrimSpace(v) != "" { defaults.BraveSearchLang = strings.TrimSpace(v) } if v, ok := raw["expand_strategy"].(string); ok && strings.TrimSpace(v) != "" { defaults.ExpandStrategy = string(libkg.ParseExpandStrategy(v)) } return defaults } func applyPatch(current storedSettings, patch SettingsPatch) storedSettings { if patch.BraveAPIKey != nil { value := strings.TrimSpace(*patch.BraveAPIKey) if value != "" && !isMaskedAPIKey(value) { current.BraveAPIKey = value } } if patch.BraveCountry != nil && strings.TrimSpace(*patch.BraveCountry) != "" { current.BraveCountry = strings.TrimSpace(*patch.BraveCountry) } if patch.BraveSearchLang != nil && strings.TrimSpace(*patch.BraveSearchLang) != "" { current.BraveSearchLang = strings.TrimSpace(*patch.BraveSearchLang) } if patch.ExpandStrategy != nil && strings.TrimSpace(*patch.ExpandStrategy) != "" { current.ExpandStrategy = string(libkg.ParseExpandStrategy(*patch.ExpandStrategy)) } return current } func (s storedSettings) toMap() map[string]interface{} { return map[string]interface{}{ "brave_api_key": s.BraveAPIKey, "brave_country": s.BraveCountry, "brave_search_lang": s.BraveSearchLang, "expand_strategy": s.ExpandStrategy, } } func toPublic(stored storedSettings) *Settings { masked := "" if stored.BraveAPIKey != "" { masked = maskAPIKey(stored.BraveAPIKey) } strategy := string(libkg.ParseExpandStrategy(stored.ExpandStrategy)) return &Settings{ BraveAPIKey: masked, BraveAPIKeyConfigured: strings.TrimSpace(stored.BraveAPIKey) != "", BraveCountry: stored.BraveCountry, BraveSearchLang: stored.BraveSearchLang, ExpandStrategy: strategy, } } func requireActor(tenantID, ownerUID string) error { if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" { return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") } return nil } func isSettingNotFound(err error) bool { if err == nil { return false } appErr := app.FromError(err) return appErr != nil && strings.Contains(strings.ToLower(appErr.Error()), "not found") } func maskAPIKey(key string) string { trimmed := strings.TrimSpace(key) if trimmed == "" { return "" } if len(trimmed) <= 4 { return "••••" } return "••••" + trimmed[len(trimmed)-4:] } func isMaskedAPIKey(value string) bool { return strings.HasPrefix(value, "••••") }