From cd985e38ea7bb3b0708dce45861731eff42eedf7 Mon Sep 17 00:00:00 2001 From: lidf Date: Sun, 12 Apr 2026 17:07:37 +0800 Subject: [PATCH] feat(archival): move client archiving from mock to API and SQLite --- backend/gateway/platforms/deepview_sse.py | 104 +++++++++++++++--- src/app/core/api.service.ts | 6 + src/app/core/client-mock.ts | 6 - .../client-ltc-dashboard.html | 16 +-- .../client-ltc-dashboard.ts | 19 +++- src/app/pages/report-detail/report-detail.ts | 31 +++--- 6 files changed, 134 insertions(+), 48 deletions(-) delete mode 100644 src/app/core/client-mock.ts diff --git a/backend/gateway/platforms/deepview_sse.py b/backend/gateway/platforms/deepview_sse.py index 0f612a7..99d3784 100644 --- a/backend/gateway/platforms/deepview_sse.py +++ b/backend/gateway/platforms/deepview_sse.py @@ -1530,23 +1530,34 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 if not user: return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS) - userDir = self._getUserStorageDir(user.get("sub", "unknown")) - clientsDir = os.path.join(userDir, "clients") - - clients = [] - if os.path.exists(clientsDir): - for d in os.listdir(clientsDir): - clientPath = os.path.join(clientsDir, d) - if os.path.isdir(clientPath): - # 尝试读取 profile.md 以获取首行做名字(可选优化) - # 默认使用目录名作为 ID 和名字 - clients.append({ - "id": d, - "name": d, - "hasProfile": os.path.exists(os.path.join(clientPath, "profile.md")) - }) - - return web.json_response({"clients": clients}, headers=_CORS_HEADERS) + try: + from hermes_state import SessionDB + db = SessionDB() + with db._lock: + cursor = db._conn.execute( + "SELECT client_id, name, phone, created_at, updated_at FROM deepview_clients WHERE user_id=? ORDER BY updated_at DESC", + (user["userId"],) + ) + rows = cursor.fetchall() + + clients = [] + for row in rows: + c_id, c_name, c_phone, c_created, c_updated = row + + # Option 1: To get report count, we could do SQL count or just skip it for now. + # Here we just map directly. + clients.append({ + "id": c_id, + "name": c_name, + "phone": c_phone, + "createdAt": c_created, + "updatedAt": c_updated + }) + + return web.json_response({"clients": clients}, headers=_CORS_HEADERS) + except Exception as e: + logger.error(f"[DeepviewSSE] DB Clients list failed: {e}") + return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS) async def _handleClientCreate(self, request: "web.Request") -> "web.Response": """ POST /deepview/clients/create @@ -1575,10 +1586,23 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 clientDir = os.path.join(userDir, "clients", clientId) os.makedirs(clientDir, exist_ok=True) - profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n" + profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n\n## 标签\n- 新客\n\n## AI 沉淀洞察\n- 暂无\n" with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f: f.write(profileData) + try: + from hermes_state import SessionDB + db = SessionDB() + with db._lock: + db._conn.execute( + "INSERT INTO deepview_clients (client_id, user_id, name, phone) VALUES (?, ?, ?, ?)", + (clientId, user["userId"], name, phone) + ) + db._conn.commit() + logger.info(f"[DeepviewSSE] Created client {clientId} in DB") + except Exception as e: + logger.error(f"[DeepviewSSE] Failed to create client {clientId} in DB: {e}") + return web.json_response({ "id": clientId, "name": name, @@ -1654,10 +1678,33 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 from hermes_state import SessionDB db = SessionDB() with db._lock: + # Update report records to associate with client + cursor = db._conn.execute("SELECT report_json FROM deepview_reports_v2 WHERE report_id=? AND user_id=?", (reportId, userId)) + row = cursor.fetchone() + + # Update clientName directly into the json to avoid joins in reports/list and report/get + if row: + try: + rData = json.loads(row[0]) + + # Find client name + c_cursor = db._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,)) + c_row = c_cursor.fetchone() + c_name = c_row[0] if c_row else "未知访客" + + rData["clientName"] = c_name + db._conn.execute("UPDATE deepview_reports_v2 SET report_json=? WHERE report_id=?", (json.dumps(rData, ensure_ascii=False), reportId)) + except: + pass + db._conn.execute( "UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?", (clientId, f"recording:{clientId}/{reportId}", reportId, userId) ) + + # Update client's updated_at timestamp to float to top in the list + db._conn.execute("UPDATE deepview_clients SET updated_at=strftime('%s','now') WHERE client_id=?", (clientId,)) + db._conn.commit() logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}") except Exception as e: @@ -1677,6 +1724,27 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数 self._app = web.Application() + # 初始化数据库 schema + try: + from hermes_state import SessionDB + db = SessionDB() + with db._lock: + db._conn.execute(""" + CREATE TABLE IF NOT EXISTS deepview_clients ( + client_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + phone TEXT DEFAULT '', + created_at REAL DEFAULT (strftime('%s','now')), + updated_at REAL DEFAULT (strftime('%s','now')) + ) + """) + db._conn.execute("CREATE INDEX IF NOT EXISTS idx_clients_user ON deepview_clients(user_id)") + db._conn.commit() + logger.info("[DeepviewSSE] DB schema for clients initialized") + except Exception as e: + logger.error(f"[DeepviewSSE] DB schema init failed: {e}") + # 注册路由 self._app.router.add_get("/deepview/health", self._handleHealth) self._app.router.add_get("/deepview/chat/history", self._handleHistory) diff --git a/src/app/core/api.service.ts b/src/app/core/api.service.ts index c294963..790072b 100644 --- a/src/app/core/api.service.ts +++ b/src/app/core/api.service.ts @@ -62,6 +62,12 @@ export class ApiService { }, { headers: this.getHeaders() }).toPromise(); } + async getClients(): Promise { + return this.http.get(`${this.apiUrl}/clients/list`, { + headers: this.getHeaders() + }).toPromise(); + } + async createClient(name: string, phone: string = ''): Promise { return this.http.post(`${this.apiUrl}/clients/create`, { name, phone diff --git a/src/app/core/client-mock.ts b/src/app/core/client-mock.ts deleted file mode 100644 index 6180562..0000000 --- a/src/app/core/client-mock.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const MOCK_CLIENTS = [ - { id: 'p_li001', name: '李梅梅', phone: '8812', age: 38, lastVisit: '2026-03-20', tags: ['乔雅登', '极度怕疼'] }, - { id: 'p_wang002', name: '王晓菲', phone: '1024', age: 29, lastVisit: '2026-04-10', tags: ['光子嫩肤'] }, - { id: 'p_zhao003', name: '赵佳佳', phone: '5566', age: 45, lastVisit: '2025-12-05', tags: ['超声炮'] }, - { id: 'p_unknown', name: '散客/未建档', phone: '', age: 0, lastVisit: '首次面诊', tags: [] } -]; diff --git a/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.html b/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.html index d2fa8ed..6bdc338 100644 --- a/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.html +++ b/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.html @@ -20,27 +20,27 @@
{{ client.name }} - - {{ getStageLabel(client.ltcStatus.stage) }} + + {{ getStageLabel(client.ltcStatus?.stage) }}
-
{{ client.gender }} · {{ client.age }}岁 · {{ client.lastVisit }} 入库
+
{{ client.gender || '女' }} · {{ client.age || '-' }}岁 · 入库时间: {{ client.createdAt ? (client.createdAt * 1000 | date:'MM-dd HH:mm') : client.lastVisit }}
- 核心诉求:{{ client.project }} + 核心诉求:{{ client.project || '待AI挖掘/人工补全' }}
-
+
⚠️ AI 诊断阻力
-
{{ client.ltcStatus.coreBarrier }}
+
{{ client.ltcStatus?.coreBarrier }}
-
+
💡 下一步建议
-
{{ client.ltcStatus.nextAction }}
+
{{ client.ltcStatus?.nextAction }}
diff --git a/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.ts b/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.ts index decfbdf..7ce29c0 100644 --- a/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.ts +++ b/src/app/pages/client-ltc-dashboard/client-ltc-dashboard.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterLink } from '@angular/router'; -import { MOCK_PATIENTS, Patient } from '../../data/mock-data'; +import { ApiService } from '../../core/api.service'; @Component({ selector: 'app-client-ltc-dashboard', @@ -11,7 +11,22 @@ import { MOCK_PATIENTS, Patient } from '../../data/mock-data'; styleUrl: './client-ltc-dashboard.css', }) export class ClientLtcDashboard { - clients = MOCK_PATIENTS; + clients: any[] = []; + + constructor(private api: ApiService) {} + + ngOnInit() { + this.loadClients(); + } + + async loadClients() { + try { + const res = await this.api.getClients(); + this.clients = res.clients || []; + } catch (e) { + console.error('Failed to load clients in LTC dashboard', e); + } + } getStageBadgeClass(stage: string): string { const map: Record = { diff --git a/src/app/pages/report-detail/report-detail.ts b/src/app/pages/report-detail/report-detail.ts index 0ece4a8..e045a1f 100644 --- a/src/app/pages/report-detail/report-detail.ts +++ b/src/app/pages/report-detail/report-detail.ts @@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'; import { RouterLink, ActivatedRoute } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { MOCK_REPORTS, Report } from '../../data/mock-data'; -import { MOCK_CLIENTS } from '../../core/client-mock'; import { ApiService } from '../../core/api.service'; import { AuthService } from '../../core/auth.service'; import { ChatContextService } from '../../core/chat-context.service'; @@ -25,8 +24,8 @@ export class ReportDetail implements OnInit { archiveStatus: 'unarchived' | 'archived' = 'unarchived'; showArchivePanel = false; searchQuery = ''; - clients = MOCK_CLIENTS; - filteredClients = MOCK_CLIENTS; + clients: any[] = []; + filteredClients: any[] = []; isArchiving = false; isCreatingClient = false; @@ -40,6 +39,17 @@ export class ReportDetail implements OnInit { const realId = decodeURIComponent(id); this.loadReportFromApi(realId); }); + this.loadClients(); + } + + async loadClients() { + try { + const res = await this.api.getClients(); + this.clients = res.clients || []; + this.filteredClients = [...this.clients]; + } catch (e) { + console.error('Failed to load clients', e); + } } /** 从后端 DB 加载静态报告,失败时降级到 localStorage 缓存 */ @@ -86,7 +96,7 @@ export class ReportDetail implements OnInit { filterClients() { const q = this.searchQuery.toLowerCase(); this.filteredClients = this.clients.filter(c => - c.name.includes(q) || c.phone.includes(q) + c.name?.toLowerCase().includes(q) || c.phone?.includes(q) ); } @@ -116,17 +126,10 @@ export class ReportDetail implements OnInit { const name = this.searchQuery.replace(/\d+/g, '').replace('尾号', '').replace(/\(\)/g, '').trim(); const res = await this.api.createClient(name, phone); - const newClient = { - id: res.id, - name: res.name, - phone: res.phone, - age: 30, - lastVisit: '首次建档', - tags: ['新客'] - }; - this.clients.push(newClient as any); - await this.archiveToClient(newClient); + this.clients.unshift(res); // Add newly created to the top + this.filteredClients = [...this.clients]; + await this.archiveToClient(res); } catch (e: any) { alert("建档失败:" + e.message); } finally {