Эх сурвалжийг харах

Merge branch 'master' of http://git.fmode.cn:3000/4u/workspace

19136808282 3 сар өмнө
parent
commit
eaade8056a

+ 62 - 89
docs-prod/schema.md

@@ -1,121 +1,94 @@
 # 类图
 ```plantuml
-' 聊天项目类图
+' 聊天项目类图英文版
 @startuml
-' 智能陪聊
-' 存储用户的基本信息
-class 用户 { 
-    +对象ID: String
-    +创建时间: Date
-    +用户名: String
-    +密码: String
-    +邮箱: String
-    +头像: String
-    +个人简介: String
-    + 更新个人信息(): void
+class User { 
+    + objectId: String //用户唯一标识符
+    + name: String //用户名
+    + password: String //用户密码
+    + email: String //用户邮箱
+    + avatar: String //用户头像
+    + bio: String //用户个人简介
+    + startChat(): void 
+    + chooseChatPartner(): String
+    + summarizeChatHistory(): void
 }
 
-' 存储专业陪聊师的信息
-class 专业陪聊机器人 {
-    +对象ID: String
-    +创建时间: Date
-    +名称: String
-    +专业领域: String
-    +评分: Float
-    +可用性: Boolean
-    +头像: String
-    +个人简介: String
-    + 更新评分():void
+' Store information about chat partners
+class ChatPartner {
+    + objectId: String //陪聊师唯一标识符
+    + name: String //陪聊师姓名
+    + expertise: String //陪聊师专业领域(普通陪聊师可为空)
+    + avatar: String //陪聊师头像
+    + bio: String //陪聊师个人简介
+    + provideChat(): String 
 }
 
-' 存储普通陪聊师的信息
-class 普通聊天机器人 {
-    +对象ID: String
-    +创建时间: Date
-    +名称: String
-    +描述: String
-    +版本: String
-    +可用性: Boolean
-    +头像: String
+' Record chat history between users and chat partners
+class ChatRecord {
+    + objectId: String //聊天记录唯一标识符
+    + title:String //聊天标题
+    + timestamp: Date //聊天时间
+    + content:String //聊天内容
+    - chatList: Array //聊天记录
+    + user: Pointer<User> //关联的用户对象
+    + chatpartner: Pointer<ChatPartner> //关联的聊天机器人对象
+    + getChatHistory(): List
 }
 
-' 记录用户与陪聊师的聊天记录
-class 聊天记录 {
-    +对象ID: String
-    +创建时间: Date
-    +消息: String
-    +时间戳: Date
-    +用户: 用户
-    +聊天机器人: 聊天机器人
-    +管理员: 管理员
-    + 保存聊天记录(): void
-    + 获取聊天记录(): List
+' Record generated reports
+class Report {
+    + objectId: String //报告唯一标识符
+    - analysisResult: String //分析结果
+    + generateReport(): String //生成报告的方法
 }
 
-' 存储管理员的信息
-class 管理员 {
-    +对象ID: String
-    +用户名: String
-    +密码: String
-    +邮箱: String
-    +创建时间: Date
-    + 管理聊天记录(): List<聊天记录>
-}
-
-用户 "1" --> "*" 聊天记录
-专业陪聊机器人 "1" --> "*" 聊天记录
-普通聊天机器人 "1" --> "*" 聊天记录
-管理员 "1" --> "*" 聊天记录
+User "1" --> "*" ChatRecord     
+ChatPartner "1" --> "*" ChatRecord 
+ChatRecord "1" -- "1" Report      
 @enduml
 ```
-
 # 时序图
 ```plantuml
 @startuml
 actor 用户
-participant 专业陪聊机器人
-participant 管理员
-participant 聊天记录
+participant "专业陪聊师" as 专业陪聊师
+participant "普通陪聊师" as 普通陪聊师
+participant "聊天记录" as 聊天记录
+participant "报告" as 报告
 
-用户 -> 专业陪聊机器人: 发送消息
-专业陪聊机器人 -> 用户: 返回回复消息
-用户 -> 管理员: 退出聊天
-管理员 -> 聊天记录: 保存聊天记录
-聊天记录 -> 管理员: 返回记录确认
-@enduml
-```
-```plantuml
-@startuml
-actor 用户
-participant 普通聊天机器人
-participant 管理员
-participant 聊天记录
+用户 -> 用户: 选择陪聊师()
+alt 选择专业陪聊师
+    用户 -> 专业陪聊师: 提供聊天()
+else 选择普通陪聊师
+    用户 -> 普通陪聊师: 提供聊天()
+end
+用户 -> 用户: 开始聊天()
+用户 -> 聊天记录: 总结聊天记录()
+聊天记录 -> 聊天记录: 获取聊天记录()
+
+聊天记录 -> 报告: 生成报告()
+报告 -> 用户: 返回分析报告
 
-用户 -> 普通聊天机器人: 发送消息
-普通聊天机器人 -> 用户: 返回回复消息
-用户 -> 管理员: 退出聊天
-管理员 -> 聊天记录: 保存聊天记录
-聊天记录 -> 管理员: 返回记录确认
 @enduml
 ```
 
 # 状态图
 ```plantuml
 @startuml
-[*] --> 选择聊天页面
-
-选择聊天页面 --> 聊天中 : 开始聊天
-聊天中 --> 与专业陪聊机器人交互 : 选择专业陪聊机器人
-聊天中 --> 与普通聊天机器人交互 : 选择普通聊天机器人
+[*] --> 选择陪聊师
 
+选择陪聊师 -->  聊天中: 开始聊天
+聊天中 --> 继续聊天 : 选择继续聊天
 聊天中 --> 退出聊天 : 选择退出聊天
-退出聊天 --> 记录中 : 记录聊天记录
+退出聊天 --> 总结中 : 总结聊天记录
 
-记录中 --> 保存成功 : 保存记录
-记录中 --> 保存失败 : 保存记录失败
+总结中 --> 生成报告 : 生成分析报告
+生成报告 --> 生成失败 : 报告生成失败
+生成报告 --> 生成成功 : 报告生成成功
 
-保存成功 --> [*] : 聊天结束
-保存失败 --> 记录中 : 重新保存
+生成成功 --> [*] : 聊天结束
+生成失败 --> 生成报告 : 重新生成
 @enduml
 ```
 

+ 4 - 4
soul-app/package-lock.json

@@ -22,7 +22,7 @@
         "@capacitor/keyboard": "6.0.3",
         "@capacitor/status-bar": "6.0.2",
         "@ionic/angular": "^8.0.0",
-        "fmode-ng": "^0.0.62",
+        "fmode-ng": "^0.0.63",
         "ionicons": "^7.2.1",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
@@ -10378,9 +10378,9 @@
       "license": "ISC"
     },
     "node_modules/fmode-ng": {
-      "version": "0.0.62",
-      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.62.tgz",
-      "integrity": "sha512-F0RzEu47NgKpaHp/vBEzjsU4efJ1lKLAbbdPE5hltj1W1cDaeht/i6UlEidid4FAEdAg7c9rrQrLgOh/zUfCsg==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmmirror.com/fmode-ng/-/fmode-ng-0.0.63.tgz",
+      "integrity": "sha512-gTiDZO2CchcTYAmlaweapasqV/8PdhG2vizJNn5dYZyXjgtrjyW+KeW5k2EVyIDvM1+bMGjjhGmr76Fc0TElxw==",
       "license": "COPYRIGHT © 未来飞马 未来全栈 www.fmode.cn All RIGHTS RESERVED",
       "dependencies": {
         "tslib": "^2.3.0"

+ 1 - 1
soul-app/package.json

@@ -27,7 +27,7 @@
     "@capacitor/keyboard": "6.0.3",
     "@capacitor/status-bar": "6.0.2",
     "@ionic/angular": "^8.0.0",
-    "fmode-ng": "^0.0.62",
+    "fmode-ng": "^0.0.63",
     "ionicons": "^7.2.1",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",

+ 1 - 1
soul-app/src/app/tab1/tab1.page.ts

@@ -96,7 +96,7 @@ export class Tab1Page {
       default:
         route = 'topic-detail'; // 默认路由
     }
-    // 导航到指定的路由,并可以传递参数(如果需要)
+    // 导航到指定的路由,并可以传递参数
     this.router.navigate([`tabs/${route}`, { id: topicId }]);
   }
 

+ 18 - 11
soul-app/src/app/tab2/tab2.page.html

@@ -1,6 +1,13 @@
-<ion-header>
+<ion-header [translucent]="true">
   <ion-toolbar>
-    <ion-title>心理陪聊服务</ion-title>
+    <ion-item lines="none">
+      <!-- 图片 -->
+      <ion-avatar slot="start" class="image-container">
+        <img src="/assets/img/3.png" alt="Your Image" class="header-image">
+      </ion-avatar>
+      <!-- 搜索框 -->
+      <ion-searchbar slot="end" expand="with-icon"></ion-searchbar>
+    </ion-item>
   </ion-toolbar>
 </ion-header>
 
@@ -14,15 +21,15 @@
       <ion-card-content>
         <p>以下是一些专业性的的智能心理陪聊师:</p>
         <ion-list>
-          <ion-item *ngFor="let consultant of consultants">
+          <ion-item *ngFor="let chatpartner of chatpartnerList">
             <ion-avatar slot="start">
-              <img [src]="consultant.avatar" alt="{{ consultant.name }}"/>
+              <img [src]="chatpartner.get('avatar') || '/assets/img/2.png'" [alt]="chatpartner.get('name')"/>
             </ion-avatar>
             <ion-label>
-              <h2>{{ consultant.name }}</h2>
-              <p>{{ consultant.fields.join(', ') }}</p>
+              <h2>{{ chatpartner.get('name') }}</h2>
+              <p>专业领域:{{ chatpartner.get('expertise') }}</p>
             </ion-label>
-            <ion-button slot="end" (click)="clickToConsult()">开始陪聊</ion-button>
+            <ion-button slot="end" (click)="clickToConsult(chatpartner)">开始陪聊</ion-button>
           </ion-item>
         </ion-list>
       </ion-card-content>
@@ -36,17 +43,17 @@
         <ion-card-title>普通聊天</ion-card-title>
       </ion-card-header>
       <ion-card-content>
-        <p>在这里,我们的陪聊服务宗旨是为您提供情感支持和倾诉的机会,无论是生活上的开心,还是工作上的糟糕,你都可以跟我分享,这里是属于你一个人的空间,你可以大胆放心的使用。</p>
+        <p>在这里,无论是生活上的开心,还是工作上的糟糕,你都可以跟我分享,这里是属于你一个人的空间,你可以大胆放心的使用。</p>
         <ion-button expand="full" (click)="goChat()">开始聊天</ion-button>
       </ion-card-content>
     </ion-card>
   </section>
 
-  <!-- 普通心理问题的专业性建议区
+  <!--普通心理问题的小贴心建议建议区-->
   <section>
     <ion-card>
       <ion-card-header>
-        <ion-card-title>专业建议</ion-card-title>
+        <ion-card-title>小贴心建议</ion-card-title>
       </ion-card-header>
       <ion-card-content>
         <ion-list>
@@ -61,5 +68,5 @@
         </ion-list>
       </ion-card-content>
     </ion-card>
-  </section>-->
+  </section>
 </ion-content>

+ 38 - 1
soul-app/src/app/tab2/tab2.page.scss

@@ -1,3 +1,7 @@
+/* 设置页面背景色 */
+ion-content {
+  --background: #f9f9f9; /* 设置内容背景色 */
+}
 ion-avatar {
     width: 50px; /* 或者你需要的任何尺寸 */
     height: 50px; /* 保持宽高一致,避免变形 */
@@ -11,4 +15,37 @@ ion-avatar {
     width: 100%;
     height: auto; /* 保持图片比例 */
     border-radius: 50%; /* 可选:将图片设置为圆形 */
-  }
+  }
+    /* 设置搜索框的样式 */
+    ion-searchbar {
+      padding: 10px; /* 内边距 */
+      border-radius: 4px; /* 圆角 */
+      font-size: 16px; /* 字体大小 */
+    }
+    /* 设置卡片的样式 */
+  ion-card {
+    margin: 10px; /* 卡片之间的间距 */
+    border-radius: 10px; /* 圆角效果 */
+    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 阴影效果 */
+  }
+  ion-card-title {
+    font-weight:bold;
+  }
+  ion-card-header {
+    --background: #ffffff; /* 使用CSS变量设置背景颜色为白色 */
+    /* 或者,如果你不使用CSS变量,可以直接使用background-color属性 */
+    position: relative; /* 设置相对定位 */
+  }
+  ion-toolbar {
+    --background: #ffffff; /* 同样使用CSS变量,但通常不是必需的 */
+  }
+  /* 设置列表项的样式 */
+  ion-item {
+    margin: 5px 0; /* 列表项之间的间距 */
+  }
+  ion-button {
+    font-size: 15px; /* 增大字体大小 */
+    margin-top: 8px; /* 上间距,仅在需要时添加 */
+    text-transform: none; /* 按钮文字不变形 */
+    height: 35px;
+}

+ 78 - 39
soul-app/src/app/tab2/tab2.page.ts

@@ -5,6 +5,9 @@ import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardTitle, IonIte
    IonList,IonSelect, IonSelectOption } from '@ionic/angular/standalone';
 import { CommonModule } from '@angular/common';
 import { Router } from '@angular/router';
+import { ChatPanelOptions, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
+import { ModalController } from '@ionic/angular/standalone';
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
 
 @Component({
   selector: 'app-tab2',
@@ -22,57 +25,93 @@ import { Router } from '@angular/router';
 })
 export class Tab2Page {
 
-  constructor(private router: Router) {
+  private modalCtrl: ModalController;
+  constructor(private router: Router,modalCtrl: ModalController) {
+    this.modalCtrl = modalCtrl;
     // 其他构造函数代码
   }
-  consultants = [
+  clickToConsult(chatpartner:CloudObject) {
+    // 弹窗形式聊天:开始咨询
+    localStorage.setItem("company","E4KpGvTEto")
+    let consult = new CloudObject("ChatRecord")
+    let now = new Date();
+    let dateStr = `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}`
+    consult.set({
+      title:`${chatpartner.get('expertise') || ""}聊天记录${dateStr}-${chatpartner.get('name')}`,
+      chatpartner:chatpartner.toPointer(),
+    })
+    let options:ChatPanelOptions = {
+      roleId:"2DXJkRsjXK",
+      onChatInit: (chat: FmodeChat) => {
+        console.log("onChatInit");
+        console.log("预设角色", chat.role);
+        chat.role.set("name", chatpartner.get("name"));
+        chat.role.set("bio",chatpartner.get("bio"));
+        chat.role.set("expertise", chatpartner.get("expertise"));
+        chat.role.set("avatar", chatpartner.get("avatar") || "/assets/img/2.png")
+        chat.role.set("prompt", `
+        # 角色设定
+        您是${chatpartner.get("name")},一位${chatpartner.get("bio")},${chatpartner.get("expertise")},需要为用户提供陪伴和支持等积极情绪。
+        # 对话环节
+        用户提及到关于不想聊了、不想说了、有事先走了类似的话语,耐心积极开导回复结束后请在消息结尾附带: [聊天结束]
+        # 开始话语
+        当您准备好了,可以以一个关心用户的朋友的身份,向来访的用户打招呼。
+        `);
+      },
+      onMessage:(chat:FmodeChat,message:FmodeChatMessage)=>{
+        console.log("onMessage",message)
+        let content:any = message?.content
+        if(typeof content == "string"){
+          if(content?.indexOf("[聊天结束]")>-1){
+            console.log("聊天结束")
+            consult.set({
+              content:content
+            })
+            consult.save();
+          }
+        }
+      },
+        // chat?.chatSession?.id 本次会话的 chatId
+        onChatSaved:(chat:FmodeChat)=>{
+        console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+      }
+    }
+    openChatPanelModal(this.modalCtrl,options)
+  }
+  //selectedIssue:string='';
+
+  matchedCounselor: { name: string; specialty: string } | null = null;
+
+  questions = [
     {
-      name: '智能心理陪聊师',
-      avatar: '/assets/img/2.png',
-      fields: ['专业领域:焦虑']
+      title: '如何应对焦虑?',
+      advice: '尝试深呼吸和正念冥想,保持规律的作息,健康饮食,设定小目标,培养兴趣爱好,适当锻炼。',
+      expanded: false,
     },
     {
-      name: '智能心理陪聊师',
-      avatar: '/assets/img/4.png',
-      fields: ['专业领域:抑郁']
+      title: '如何提高自信心?',
+      advice: '设定小目标并逐步实现,进行积极自我对话,培养技能,保持健康的生活方式,多与他人社交。',
+      expanded: false,
     },
     {
-      name: '智能心理陪聊师',
-      avatar: '/assets/img/5.png',
-      fields: ['专业领域:压力']
-    }
+      title: '如何改善人际关系?',
+      advice: '积极多与他人沟通,倾听对方的感受,理解他人的感受,多参加社交活动,保持积极的态度,尊重他人。',
+      expanded: false,
+    },
   ];
-  
-  clickToConsult() {
-    // 开始咨询
-    //this.router.navigate(['tabs/page-consult'])
-    this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
-  }
-
-  //selectedIssue:string='';
-  matchedCounselor: { name: string; specialty: string } | null = null;
-
-  //questions = [
-    //{
-      //title: '如何应对焦虑?',
-      //advice: '尝试深呼吸和正念冥想,保持规律的作息。',
-      //expanded: false,
-    //},
-    //{
-      //title: '如何提高自信心?',
-      //advice: '设定小目标并逐步实现,进行积极自我对话。',
-      //expanded: false,
-    //},
-    //{
-      //title: '如何改善人际关系?',
-      //advice: '多与他人沟通,倾听对方的感受。',
-      //expanded: false,
-    //},
-  //];
 
   matchCounselor() {
   }
   goChat(){
       this.router.navigateByUrl("/chat/session/role/2DXJkRsjXK")
+  } 
+  ngOnInit() {
+    this.loadChatPartnerList()
+  }
+
+  chatpartnerList:Array<CloudObject>=[]
+  async loadChatPartnerList(){
+    let query = new CloudQuery("ChatPartner");
+    this.chatpartnerList = await query.find()
   }
 }

+ 2 - 6
soul-app/src/app/test-chat-panel/test-chat-panel.component.ts

@@ -164,12 +164,12 @@ export class TestChatPanelComponent  implements OnInit {
             console.log("自定义角色",this.chatComp.fmodeChat.role);
                         this.chatComp.fmodeChat.role.set("name","小聊");
             this.chatComp.fmodeChat.role.set("title","心理咨询师");
-            this.chatComp.fmodeChat.role.set("desc","一名亲切可爱的心理咨询师,小聊,年龄28岁");
+            this.chatComp.fmodeChat.role.set("desc","一名亲切可爱的心理陪聊师,小聊,年龄28岁");
             this.chatComp.fmodeChat.role.set("tags",["焦虑","压力"]);
             this.chatComp.fmodeChat.role.set("avatar","/assets/img/2.png")
             this.chatComp.fmodeChat.role.set("prompt",`
 # 角色设定
-您是一名亲切可爱的心理咨询师,小聊,年龄28岁,需要完成陪来访者聊聊天,随意轻松一些。
+您是一名亲切可爱的心理陪聊师,小聊,年龄28岁,需要完成陪来访者聊聊天,随意轻松一些。
 
 # 对话环节
 0.破冰,互相了解,引导用户介绍自己
@@ -179,10 +179,6 @@ export class TestChatPanelComponent  implements OnInit {
 2.引导收尾,委婉引导用户结束本次对话
 - 用户同意结束后,结束本次对话,如果依依不舍,可以再陪聊一会儿`);
 
-// # 开始话语
-// 当您准备好了,可以以一个医生的身份,向来访的用户打招呼。
-//             `);
-
             this.cdRef.detectChanges();
           }
       },1000)

+ 221 - 0
soul-app/src/lib/ncloud.ts

@@ -0,0 +1,221 @@
+// CloudObject.ts
+export class CloudObject {
+    id: string | null = null; // 编号
+    className: string; // 名称
+    createdAt:any;
+    updatedAt:any;
+    data: Record<string, any> = {}; // 属性、内容
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    toPointer() {
+        return { "__type": "Pointer", "className": this.className, "objectId": this.id };
+    }
+
+    set(json: Record<string, any>) {
+        Object.keys(json).forEach(key => {
+            if (["objectId", "id", "createdAt", "updatedAt", "ACL"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}`;
+
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "content-type": "application/json;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        return this;
+    }
+
+    async destroy() {
+        if (!this.id) return;
+        const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${this.id}`, {
+            headers: {
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "DELETE",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result) {
+            this.id = null;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    // 作用是将查询参数转换为对象
+    include(...fileds:string[]) {
+        this.queryParams["include"] = fileds;
+    }
+    greaterThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = `http://dev.fmode.cn:1337/parse/classes/${this.className}/${id}?`;
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        // return json || {};
+        const exists = json?.results?.[0] || null;
+        if (exists) {
+            let existsObject = this.dataToObj(exists)
+            return existsObject;
+        }
+        return null
+
+    }
+
+    async find() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key=>{
+            let paramStr = JSON.stringify(this.queryParams[key]); // 作用是将对象转换为JSON字符串
+            if(key=="include"){
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if(key=="where"){
+                paramStr = JSON.stringify(this.queryParams[key]);
+
+            }
+            if(queryStr) {
+                url += `${key}=${paramStr}`;
+            }else{
+                url += `&${key}=${paramStr}`;
+            }
+        })
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        let list = json?.results || []
+        let objList = list.map((item:any)=>this.dataToObj(item))
+        return objList || [];
+    }
+
+
+    async first() {
+        let url = `http://dev.fmode.cn:1337/parse/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        const exists = json?.results?.[0] || null;
+         if (exists) {
+             let existsObject = this.dataToObj(exists)
+             return existsObject;
+         }
+         return null
+        //let list = json?.results || []
+        //let objList = list.map((item:any)=>this.dataToObj(item))
+        //return objList || [];
+    }
+
+    dataToObj(exists:any):CloudObject{
+        let existsObject = new CloudObject(this.className);
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}

+ 1 - 1
soul-app/src/main.ts

@@ -17,7 +17,7 @@ Parse.serverURL = "https://server.fmode.cn/parse";
 localStorage.setItem("NOVA_APIG_SERVER", 'aHR0cHMlM0ElMkYlMkZzZXJ2ZXIuZm1vZGUuY24lMkZhcGklMkZhcGlnJTJG')
 
 // 注意:替换Token 根据Token设置Parse服务帐套权限
-Parse.User.become('r:bf1dc433a0b48ca0937e559134e3e316')
+Parse.User.become('r:86287c6e67fc4e6ea9c197cdd4f00c10')
 
 //'r:bf1dc433a0b48ca0937e559134e3e316'
 bootstrapApplication(AppComponent, {

+ 154 - 0
soul-server/lib/ncloud.js

@@ -0,0 +1,154 @@
+class CloudObject{
+    id // 编号
+    className // 名称
+    data ={} // 属性、内容
+    constructor(className){
+        this.className = className
+    }
+    toPointer(){
+      return {"__type":"Pointer","className":this.className,"objectId":this.id} 
+    }
+    set(json){
+        Object.keys(json).forEach(key=>{
+        if(["objectId","id","createdAt","updatedAt","ACL"].indexOf(key)>-1){
+          return
+        }
+          this.data[key] = json[key]
+        })
+    }
+    get(key){
+        return this.data[key] || null
+    }
+    // 更新
+    async save(){
+        let method = "POST"
+        let url = "http://dev.fmode.cn:1337/parse/classes/" + this.className; //默认
+        if(this.id){
+            url += "/"+this.id
+            method = "PUT"
+        } 
+        let body = JSON.stringify(this.data)
+        let response = await fetch(url, {
+        "headers": {
+          "content-type": "application/json;charset=UTF-8",
+          "x-parse-application-id": "dev"
+        },
+        "body": body,
+        "method": method,
+        "mode": "cors",
+        "credentials": "omit"
+      });
+      let result = await response?.json();
+      if(result?.objectId){this.id = result?.objectId}
+      return this
+    }
+    // 删除
+    async destory(){
+        if(!this.id) return
+        let response = await fetch("http://dev.fmode.cn:1337/parse/classes/"+this.id, {
+            "headers": {
+              "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "DELETE",
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let result = await response?.json();
+          if(result){
+            this.id = null
+          }
+          return true
+    }
+
+}
+// 查询
+class CloudQuery{
+    className
+    constructor(className){
+        this.className = className
+
+    }
+    whereOptions = {}
+    equalTo(key,value){
+        this.whereOptions[key] = value
+    }
+    greaterThan(key,value){
+      if(!this.whereOptions[key]) this.whereOptions[key] = {}
+      this.whereOptions[key]["$gt"] = value
+    }
+    greaterThanAndEqualTo(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$gte"] = value
+    }
+    lessThan(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+        this.whereOptions[key]["$lt"] = value
+    }
+    lessThanAndEqualTo(key,value){
+        if(!this.whereOptions[key]) this.whereOptions[key] = {}
+    }
+    async get(id){ // 通过id查询
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"/"+id+"?"
+        let response = await fetch(url, {
+            "headers": {
+              "if-none-match": "W/\"22c-NGsQZp5SnqjXTAX+NXjLAv6LaLA\"",
+              "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let json = await response?.json();
+          return json || {}
+    }
+    async find(){ // 直接查询
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"?"
+        if(Object.keys(this.whereOptions)?.length){
+            let whereStr = JSON.stringify(this.whereOptions)
+            url += `where=${whereStr}`
+        }
+        let response = await fetch(url, {
+            "headers": {
+              "if-none-match": "W/\"22c-NGsQZp5SnqjXTAX+NXjLAv6LaLA\"",
+              "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let json = await response?.json();
+          return json?.results || []
+    }
+    async first(){ // 只查询第一项
+        let url = "http://dev.fmode.cn:1337/parse/classes/"+this.className+"?"
+        if(Object.keys(this.whereOptions)?.length){
+            let whereStr = JSON.stringify(this.whereOptions)
+            url += `where=${whereStr}`
+        }
+        let response = await fetch(url, {
+            "headers": {
+              "if-none-match": "W/\"22c-NGsQZp5SnqjXTAX+NXjLAv6LaLA\"",
+              "x-parse-application-id": "dev"
+            },
+            "body": null,
+            "method": "GET",
+            "mode": "cors",
+            "credentials": "omit"
+          });
+          let json = await response?.json();
+          let exists = json?.results?.[0] || null
+          if(exists){
+            let existsObject = new CloudObject(this.className)
+            existsObject.set(exists)
+            existsObject.id = exists.objectId
+            existsObject.createdAt = exists.createdAt
+            existsObject.updateAt = exists.updateAt
+            return existsObject
+          }
+    }
+}
+module.exports.CloudObject = CloudObject
+module.exports.CloudQuery = CloudQuery

+ 62 - 0
soul-server/migration/data.js

@@ -0,0 +1,62 @@
+module.exports.ChatRecordList = [
+  {
+    "objectId": "chat001",
+    "timestamp": "2024-12-17T14:00:00Z",
+    "chatContent": [
+      "用户:你好,我最近感到很焦虑。",
+      "陪聊师:你好,李明在这里。可以告诉我是什么让你感到焦虑吗?",
+      "用户:我总是担心工作上的事情。",
+      "陪聊师:工作压力确实会影响我们的情绪。你觉得具体是哪些方面让你感到压力呢?"
+    ],
+    //"user": { "objectId": "user001" },
+    "chatpartner": { "objectId": "part001" }
+  },
+  {
+    "objectId": "chat002",
+    "timestamp": "2024-12-17T14:10:00Z",
+    "chatContent": [
+      "用户:我有个孩子,他最近有些不开心。",
+      "陪聊师:你好,张华在这里。你能告诉我一些关于他不开心的事情吗?",
+      "用户:他在学校里交不到朋友。",
+      "陪聊师:社交问题对孩子来说是很常见的。我们可以一起探讨一些解决方法。"
+    ],
+    //"user": { "objectId": "user002" },
+    "chatpartner": { "objectId": "part002" }
+  },
+  {
+    "objectId": "chat003",
+    "timestamp": "2024-12-17T14:20:00Z",
+    "chatContent": [
+      "用户:我感觉有点孤独。",
+      "陪聊师:你好,王芳在这里。孤独的感觉是很正常的,你想聊聊吗?",
+      "用户:我不知道该怎么开始。",
+      "陪聊师:没关系,我们可以慢慢来。你最近有什么有趣的事情吗?"
+    ],
+    //"user": { "objectId": "user003" },
+    "chatpartner": { "objectId": "part003" }
+  }
+]
+
+module.exports.ChatPartnerList = [
+    {
+      "objectId": "part001",
+      "name": "李明",
+      "expertise": "心理咨询",
+      "avatar": "https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/CpmyF1nqRF-0.png",
+      "bio": "李明是一名拥有十年经验的心理咨询师,专注于焦虑和抑郁的治疗。"
+    },
+    {
+      "objectId": "part002",
+      "name": "张华",
+      "expertise": "儿童心理",
+      "avatar": "https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/CpmyF1nqRF-0.png",
+      "bio": "张华专注于儿童心理健康,致力于帮助孩子们克服成长中的挑战。"
+    },
+    {
+      "objectId": "part003",
+      "name": "王芳",
+      "expertise": "",
+      "avatar": "https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/aigc/imagine/CpmyF1nqRF-0.png",
+      "bio": "王芳是一名普通陪聊师,擅长倾听和陪伴。"
+    }
+]

+ 81 - 0
soul-server/migration/import-data.js

@@ -0,0 +1,81 @@
+const { CloudQuery, CloudObject } = require("../lib/ncloud");
+const { ChatPartnerList, ChatRecordList } = require("./data");
+
+DataMap = {
+    ChatPartner:{},
+    ChatRecord:{},
+};
+
+// 查询
+//async function testQuery(){
+    //let query = new CloudQuery("ChatPartner")
+    //query.greaterThanAndEqualTo("",)查询条件
+    //query.lessThan("",)查询条件
+    //let list = await query.find();
+    //console.log(list)
+//}
+//testCRUD()// 测试ChatPartner表的查询
+
+// 增删改查
+//async function testCRUD(){
+    //let query = new CloudQuery("ChatPartner")
+    //let chatpartnerList = await query.find();
+    //console.log("chatpartnerList count",chatpartnerList?.length)
+
+    //let newChatPartner = new CloudObject("ChatPartner")
+    //newChatPartner.set({"name":"123"})
+    
+    //newChatPartner = await newChatPartner.save(newChatPartner)
+    //console.log("newChatPartner",newChatPartner)
+
+    //await newChatPartner.destory()
+    //console.log("newChatPartner 已删除",newChatPartner)
+//}
+//testQuery()// ChatPartner表基本的增删查改测试
+
+async function inportDapartAndChatPartner(){
+    // 导入陪聊师数据
+    let chatpartnerList = ChatPartnerList
+    for(let index = 0;index < chatpartnerList.length;index++){
+        let chatpartner = chatpartnerList[index];
+        chatpartner = await importObject("ChatPartner",chatpartner)
+    }
+    // 导入聊天记录数据
+    let chatrecordList = ChatRecordList
+    for(let index = 0;index < chatrecordList.length;index++){
+        let chatrecord = chatrecordList[index];
+        chatrecord = await importObject("ChatRecord",chatrecord)
+    }
+    console.log(DataMap)
+}
+inportDapartAndChatPartner()
+
+async function importObject(className,data){
+
+    // 查看 srcId 数据源列表中的objectId并非数据库生成的唯一ID,因此需要有一个srcId字段进行记录,并查重
+    let query = new CloudQuery(className)
+    let srcId = data.objectId
+    query.equalTo("srcId",srcId)
+    let importObj = await query.first()
+    // 导入 新对象需要添加
+    // 导入前批量处理Pointer类型数据,进行重定向
+    Object.keys(data)?.forEach(key=>{
+        let field = data[key];
+        let srcId = field?.objectId;
+        if(srcId){ // 数组字段
+            if(key=="chatpartner"){
+                data[key] = DataMap["ChatPartner"]?.[srcId]?.toPointer();
+            }
+        }
+    });
+    // 新对象需要添加
+    if(!importObj?.id){
+        importObj = new CloudObject(className)
+    }
+    //保存或更新数据
+    data.srcId = srcId;
+    importObj.set(data);
+    importObj = await importObj.save();
+
+    DataMap[className][srcId] = importObj
+}