package radarmap import ( "apps/backend/internal/module/radar/domain" "apps/backend/internal/types" ) /* ServiceProfile 把 domain 檔案轉成 API 形狀。 p 為 nil 代表使用者還沒建檔:回 exists=false 的空殼,而不是 404 —— 表單本來就要能開空的。 但 exists 這個欄位必須誠實,訂閱閘門(SP-01)與前端引導都看它。 */ func ServiceProfile(p *domain.ServiceProfile) *types.ServiceProfilePublic { if p == nil { return &types.ServiceProfilePublic{ Exists: false, Services: []types.ServiceItem{}, Cases: []types.ServiceCasePublic{}, Forbidden: []string{}, Faq: []types.FaqItem{}, ServiceAreas: []string{}, } } services := make([]types.ServiceItem, 0, len(p.Services)) for _, s := range p.Services { services = append(services, types.ServiceItem{ Name: s.Name, PriceMin: s.PriceMin, PriceMax: s.PriceMax, Currency: s.Currency, }) } cases := make([]types.ServiceCasePublic, 0, len(p.Cases)) for _, c := range p.Cases { cases = append(cases, types.ServiceCasePublic{Title: c.Title, Summary: c.Summary, Link: c.Link}) } faq := make([]types.FaqItem, 0, len(p.Faq)) for _, f := range p.Faq { faq = append(faq, types.FaqItem{Question: f.Question, Answer: f.Answer}) } forbidden := p.Forbidden if forbidden == nil { forbidden = []string{} } areas := p.ServiceAreas if areas == nil { areas = []string{} } return &types.ServiceProfilePublic{ Exists: true, Services: services, Cases: cases, Forbidden: forbidden, Faq: faq, ServiceAreas: areas, RemoteOk: p.RemoteOk, Availability: p.Availability, ToneNote: p.ToneNote, UpdatedAt: p.UpdatedAt, } } func Watch(w *domain.RadarWatch) *types.RadarWatchPublic { if w == nil { return nil } terms, excludes, regions := w.Terms, w.ExcludeTerms, w.Regions if terms == nil { terms = []string{} } if excludes == nil { excludes = []string{} } if regions == nil { regions = []string{} } return &types.RadarWatchPublic{ Id: w.ID, Terms: terms, ExcludeTerms: excludes, Regions: regions, Status: w.Status, LastSweptAt: w.LastSweptAt, CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, } } func WatchList(list []*domain.RadarWatch) []types.RadarWatchPublic { out := make([]types.RadarWatchPublic, 0, len(list)) for _, w := range list { if p := Watch(w); p != nil { out = append(out, *p) } } return out } func Pagination(page, pageSize int, total int64) types.Pagination { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } tp := int(total) / pageSize if int(total)%pageSize != 0 { tp++ } return types.Pagination{Page: page, PageSize: pageSize, Total: total, TotalPages: tp} } // ServiceProfileInput 把 PUT 的 request 轉成 domain。owner_uid 一律由 logic 從 JWT 帶入。 func ServiceProfileInput(req *types.UpsertServiceProfileReq) *domain.ServiceProfile { if req == nil { return nil } services := make([]domain.ServiceItem, 0, len(req.Services)) for _, s := range req.Services { services = append(services, domain.ServiceItem{ Name: s.Name, PriceMin: s.PriceMin, PriceMax: s.PriceMax, Currency: s.Currency, }) } cases := make([]domain.ServiceCase, 0, len(req.Cases)) for _, c := range req.Cases { cases = append(cases, domain.ServiceCase{Title: c.Title, Summary: c.Summary, Link: c.Link}) } faq := make([]domain.FaqItem, 0, len(req.Faq)) for _, f := range req.Faq { faq = append(faq, domain.FaqItem{Question: f.Question, Answer: f.Answer}) } return &domain.ServiceProfile{ Services: services, Cases: cases, Forbidden: req.Forbidden, Faq: faq, ServiceAreas: req.ServiceAreas, RemoteOk: req.RemoteOk, Availability: req.Availability, ToneNote: req.ToneNote, } } func Opportunity(o *domain.Opportunity) *types.OpportunityPublic { if o == nil { return nil } reasons := make([]types.OpportunityReason, 0, len(o.Reasons)) for _, r := range o.Reasons { reasons = append(reasons, types.OpportunityReason{Dimension: r.Dimension, Score: r.Score, Reason: r.Reason}) } terms := o.MatchedTerms if terms == nil { terms = []string{} } out := &types.OpportunityPublic{ Id: o.ID, WatchId: o.WatchID, Source: o.Source, SourceScoutPostId: o.SourceScoutPostID, ExternalId: o.ExternalID, Permalink: o.Permalink, AuthorHandle: o.AuthorHandle, Text: o.Text, PostedAt: o.PostedAt, Status: o.Status, IntentScore: o.IntentScore, IntentBand: o.IntentBand, Reasons: reasons, RegionDetected: o.RegionDetected, RegionMatch: o.RegionMatch, FreshnessHours: o.FreshnessHours, MatchedService: o.MatchedService, MatchedTerms: terms, RejectReason: o.RejectReason, ContactId: o.ContactID, CreatedAt: o.CreatedAt, } if o.Override != nil { out.Override = &types.OpportunityOverride{ FromBand: o.Override.FromBand, ToBand: o.Override.ToBand, FromStatus: o.Override.FromStatus, ToStatus: o.Override.ToStatus, ActorUid: o.Override.ActorUID, At: o.Override.At, } } return out } func OpportunityList(list []*domain.Opportunity) []types.OpportunityPublic { out := make([]types.OpportunityPublic, 0, len(list)) for _, o := range list { if p := Opportunity(o); p != nil { out = append(out, *p) } } return out } func Sweep(s *domain.RadarSweep) *types.RadarSweepPublic { if s == nil { return nil } return &types.RadarSweepPublic{ Id: s.ID, WatchId: s.WatchID, JobId: s.JobID, Path: s.Path, HitCount: s.HitCount, JudgedCount: s.JudgedCount, CreatedCount: s.CreatedCount, TruncatedCount: s.TruncatedCount, FailedReason: s.FailedReason, CreditsUsed: s.CreditsUsed, StartedAt: s.StartedAt, EndedAt: s.EndedAt, } } func SweepList(list []*domain.RadarSweep) []types.RadarSweepPublic { out := make([]types.RadarSweepPublic, 0, len(list)) for _, s := range list { if p := Sweep(s); p != nil { out = append(out, *p) } } return out } func Reply(r *domain.ReplyVariant) *types.ReplyVariantPublic { if r == nil { return nil } return &types.ReplyVariantPublic{ Id: r.ID, OpportunityId: r.OpportunityID, Variant: r.Variant, Text: r.Text, UsedAt: r.UsedAt, SentChannel: r.SentChannel, CreatedAt: r.CreatedAt, } } func ReplyList(list []*domain.ReplyVariant) []types.ReplyVariantPublic { out := make([]types.ReplyVariantPublic, 0, len(list)) for _, r := range list { if p := Reply(r); p != nil { out = append(out, *p) } } return out }