import { Injectable } from '@angular/core'; import { AuthCreditService } from './auth-credit.service'; import { DouyinInsightService } from './douyin-insight.service'; import { TopicIdea } from '../models/douyin-insight.model'; const TOPIC_POOL_STORAGE_KEY = 'videoWorkflow.topicPool.items'; @Injectable({ providedIn: 'root' }) export class TopicPoolService { constructor( private auth: AuthCreditService, private insight: DouyinInsightService, ) {} listTopics(): TopicIdea[] { const userId = this.requireUserId(); return this.readAll() .filter((item) => item.userId === userId) .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)); } async refreshFromCloud(): Promise { const userId = this.requireUserId(); const cloudItems = await this.insight.listTopics(); if (!cloudItems.length) return this.listTopics(); const allLocal = this.readAll(); const others = allLocal.filter((item) => item.userId !== userId); const currentLocal = allLocal.filter((item) => item.userId === userId); const merged = new Map(currentLocal.map((item) => [item.id, item])); for (const cloudItem of cloudItems) { const localItem = merged.get(cloudItem.id); const keepLocal = localItem && Date.parse(localItem.updatedAt) > Date.parse(cloudItem.updatedAt); merged.set(cloudItem.id, keepLocal ? localItem : cloudItem); } const current = Array.from(merged.values()); const next = [...others, ...current]; this.writeAll(next); return current.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)); } saveTopic(input: Omit): TopicIdea { const userId = this.requireUserId(); const now = new Date().toISOString(); const topic: TopicIdea = { ...input, id: this.createId(), userId, createdAt: now, updatedAt: now, }; const items = this.readAll(); items.unshift(topic); this.writeAll(items); this.mirrorCreate(topic); return topic; } updateTopic(id: string, patch: Partial): TopicIdea { const userId = this.requireUserId(); const items = this.readAll(); const index = items.findIndex((item) => item.id === id && item.userId === userId); if (index < 0) throw new Error('未找到该选题'); const updated: TopicIdea = { ...items[index], ...patch, id: items[index].id, userId, updatedAt: new Date().toISOString(), }; items[index] = updated; this.writeAll(items); this.mirrorUpdate(updated, patch); return updated; } archiveTopic(id: string): TopicIdea { return this.updateTopic(id, { status: 'archived' }); } private mirrorCreate(topic: TopicIdea): void { if (!this.auth.isLoggedIn) return; this.insight.createTopic(topic).catch((err) => { if (topic.sourceType === 'ip_operator') { this.insight.createTopic(this.cloudCompatibleTopic(topic)).catch((fallbackErr) => { console.warn('[TopicPoolService] IP operator topic cloud fallback create failed; local data kept:', fallbackErr?.message || fallbackErr); }); return; } console.warn('[TopicPoolService] 云端保存选题失败,已保留本地数据:', err?.message || err); }); } private mirrorUpdate(topic: TopicIdea, patch: Partial): void { if (!this.auth.isLoggedIn) return; this.insight.updateTopic(topic.id, patch).catch((err) => { if (topic.sourceType === 'ip_operator') { this.insight.updateTopic(topic.id, this.cloudCompatiblePatch(patch)).catch((fallbackErr) => { if (this.isMissingCloudObject(fallbackErr)) { this.insight.createTopic(this.cloudCompatibleTopic(topic)).catch((createErr) => { console.warn('[TopicPoolService] IP operator topic cloud fallback create after update miss failed; local data kept:', createErr?.message || createErr); }); return; } console.warn('[TopicPoolService] IP operator topic cloud fallback update failed; local data kept:', fallbackErr?.message || fallbackErr); }); return; } if (this.isMissingCloudObject(err)) { this.insight.createTopic(topic).catch((createErr) => { console.warn('[TopicPoolService] 云端补建选题失败,已保留本地数据:', createErr?.message || createErr); }); return; } console.warn('[TopicPoolService] 云端更新选题失败,已保留本地数据:', err?.message || err); }); } private cloudCompatibleTopic(topic: TopicIdea): TopicIdea { if (topic.sourceType !== 'ip_operator') return topic; return { ...topic, sourceType: 'manual', tags: Array.from(new Set(['IP操盘', ...(topic.tags || [])])), sourceTitle: topic.sourceTitle || 'IP操盘方案', sourceSummary: topic.sourceSummary || '由 IP操盘工作台同步,云端兼容保存为手动来源。', }; } private cloudCompatiblePatch(patch: Partial): Partial { const next: Partial = { ...patch }; if (next.sourceType === 'ip_operator') { next.sourceType = 'manual'; next.tags = Array.from(new Set(['IP操盘', ...(next.tags || [])])); } return next; } private isMissingCloudObject(error: any): boolean { const message = [ error?.message, error?.response?.error, error?.response?.raw?.message, error?.detail?.error, error?.detail?.raw?.message, ].filter(Boolean).join(' '); return /object not found|未找到记录|not found/i.test(message); } private requireUserId(): string { return this.auth.currentUser?.objectId || 'local-guest'; } private readAll(): TopicIdea[] { try { const raw = localStorage.getItem(TOPIC_POOL_STORAGE_KEY); return raw ? JSON.parse(raw) as TopicIdea[] : []; } catch { return []; } } private writeAll(items: TopicIdea[]): void { localStorage.setItem(TOPIC_POOL_STORAGE_KEY, JSON.stringify(items)); } private createId(): string { return `topic_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } }