| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- 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<TopicIdea[]> {
- 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, 'id' | 'userId' | 'createdAt' | 'updatedAt'>): 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>): 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<TopicIdea>): 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<TopicIdea>): Partial<TopicIdea> {
- const next: Partial<TopicIdea> = { ...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)}`;
- }
- }
|