Procházet zdrojové kódy

feat:history consult

Mads před 6 dny
rodič
revize
3252208cbd

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "plantuml.server": "https://www.plantuml.com/plantuml"
+}

+ 130 - 0
docs/schema.md

@@ -0,0 +1,130 @@
+#AI法律咨询项目
+
+#数据范式设计
+>设定要求
+您是一名专业的数据库工程师,熟悉PostgreSQL和ParseSever。
+请注意表名用大驼峰,字段小驼峰。
+有预留字段:objectId、updatedAt、createdAt。
+关于ParseSever中数据类的描述,字段的主要类型有:
+String => String
+Number => Number
+Bool => bool
+Array => JSON Array
+Object => JSON Object
+Date => Date
+File => Parse.File
+Pointer => other Parse.Object
+Relation => Parse.Relation
+Null => null
+GeoPoint => {latitude: 40.0, longitude: -30.0}
+
+>项目需求
+相关法律咨询的辅助AI应用,咨询者(_User)、劳动合同、离婚协议书、借款合同、房屋租赁合同、股东协议、著作权转让协,请您根据法律咨询的行业经验,设计以上7张表,咨询者直接用预留的_User表即可。
+
+>输出结果(uml类图)
+请您帮我用plantuml的类图描述设计好的几张表及其关系
+
+>输出结果(信息结构图)
+请您帮我用markmap格式表示上面的信息结构图
+
+>输出结果(SQL语句)
+请您帮我用SQL格式给我建表语句和测试数据插入语句
+
+#UML类图
+
+```plantuml
+@startuml
+skinparam groupInheritance 2
+
+class _User {
+  + objectId: String <<PK>>
+  + username: String
+  + password: String
+  + email: String
+  + phone: String
+  + createdAt: Date
+  + updatedAt: Date
+}
+
+class LaborContract {
+  + objectId: String <<PK>>
+  + title: String
+  + employer: String
+  + employee: String
+  + contractPeriod: Number
+  + probation: Number
+  + salaryDetails: JSON Object
+  + terminationClauses: String
+  + signDate: Date
+  + user: Pointer > _User
+}
+
+class DivorceAgreement {
+  + objectId: String <<PK>>
+  + partyA: String
+  + partyB: String
+  + divisionAssets: JSON Array
+  + childCustody: String
+  + alimony: Number
+  + effectiveDate: Date
+  + user: Pointer > _User
+}
+
+class LoanContract {
+  + objectId: String <<PK>>
+  + lender: String
+  + borrower: String
+  + principal: Number
+  + interestRate: Number
+  + repaymentSchedule: JSON Object
+  + collateral: String
+  + dueDate: Date
+  + user: Pointer > _User
+}
+
+class HouseLease {
+  + objectId: String <<PK>>
+  + landlord: String
+  + tenant: String
+  + propertyAddress: JSON Object
+  + leaseTerm: Number
+  + monthlyRent: Number
+  + deposit: Number
+  + facilities: JSON Array
+  + startDate: Date
+  + user: Pointer > _User
+}
+
+class ShareholderAgreement {
+  + objectId: String <<PK>>
+  + companyName: String
+  + shareholders: JSON Array
+  + capitalContributions: JSON Object
+  + votingRights: JSON Object
+  + profitDistribution: String
+  + disputeResolution: String
+  + user: Pointer > _User
+}
+
+class CopyrightTransfer {
+  + objectId: String <<PK>>
+  + copyrightOwner: String
+  + transferee: String
+  + workTitle: String
+  + transferRights: JSON Array
+  + territory: String
+  + compensation: Number
+  + effectiveDate: Date
+  + user: Pointer > _User
+}
+
+' 关联关系
+_User "1" -- "n" LaborContract
+_User "1" -- "n" DivorceAgreement
+_User "1" -- "n" LoanContract
+_User "1" -- "n" HouseLease
+_User "1" -- "n" ShareholderAgreement
+_User "1" -- "n" CopyrightTransfer
+@enduml
+
+```

+ 1 - 0
myapp/.vscode/settings.json

@@ -1,3 +1,4 @@
 {
   "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"]
 }
+

+ 79 - 30
myapp/src/app/tab2/tab2.page.html

@@ -1,41 +1,90 @@
 <ion-header [translucent]="true">
   <ion-toolbar>
-    <ion-title>
-      AI法律咨询
-    </ion-title>
+    <ion-title>AI法律咨询</ion-title>
   </ion-toolbar>
 </ion-header>
 
 <ion-content [fullscreen]="true">
   <ion-card class="lawyer-card">
-  <div class="card-header">
-    <div class="avatar-container">
-      <img src="/assets/lin.jpg" alt="林正鸿律师" class="lawyer-avatar">
+    <div class="card-header">
+      <div class="avatar-container">
+        <img src="/assets/lin.jpg" alt="林正鸿律师" class="lawyer-avatar">
+      </div>
+      <div class="header-text">
+        <h2>林正鸿</h2>
+        <p class="title">国正律师事务所 创始合伙人</p>
+      </div>
     </div>
-    <div class="header-text">
-      <h2>林正鸿</h2>
-      <p class="title">国正律师事务所 创始合伙人</p>
-    </div>
-  </div>
 
-  <ion-card-content>
-    <div class="specialty-tags">
-      <ion-chip color="primary">刑事辩护</ion-chip>
-      <ion-chip color="primary">婚姻家庭</ion-chip>
-      <ion-chip color="primary">合同纠纷</ion-chip>
-    </div>
-    
-    <div class="profile-desc">
-      <p><ion-icon name="school-outline"></ion-icon> 全国优秀律师 | 全国律协刑事专业委员会副主任</p>
-      <p><ion-icon name="document-text-outline"></ion-icon> 擅长经济犯罪辩护、企业合规及司法政策研究</p>
-      <p><ion-icon name="ribbon-outline"></ion-icon> 参与多项最高法司法解释制定工作</p>
-    </div>
-  </ion-card-content>
+    <ion-card-content>
+      <div class="specialty-tags">
+        <ion-chip color="primary">刑事辩护</ion-chip>
+        <ion-chip color="primary">婚姻家庭</ion-chip>
+        <ion-chip color="primary">合同纠纷</ion-chip>
+      </div>
+      
+      <div class="profile-desc">
+        <p><ion-icon name="school-outline"></ion-icon> 全国优秀律师 | 全国律协刑事专业委员会副主任</p>
+        <p><ion-icon name="document-text-outline"></ion-icon> 擅长经济犯罪辩护、企业合规及司法政策研究</p>
+        <p><ion-icon name="ribbon-outline"></ion-icon> 参与多项最高法司法解释制定工作</p>
+      </div>
+    </ion-card-content>
+
+    <!-- 修改后的咨询卡片部分,移除了row/col布局 -->
+    <ion-card class="consultation-card">
+      <ion-card-header class="ion-text-center">
+        <ion-icon name="document-text-outline" color="primary" class="feature-icon" style="font-size: 48px;"></ion-icon>
+        <ion-card-title class="ion-padding-top">法律咨询服务</ion-card-title>
+        <ion-card-subtitle>专业法律咨询 - 维护您的合法权益</ion-card-subtitle>
+      </ion-card-header>
+
+      <ion-card-content>
+        <ion-text color="medium" class="ion-text-center ion-margin-bottom">
+          <p>我们的专业律师团队随时为您提供法律咨询服务</p>
+        </ion-text>
+
+        @if (consultList) {
+          <ion-list lines="none" class="consult-history">
+            <ion-list-header>
+              <ion-label>历史咨询记录</ion-label>
+              <ion-button fill="clear" size="small" (click)="loadConsult()">
+                <ion-icon slot="icon-only" name="refresh"></ion-icon>
+              </ion-button>
+            </ion-list-header>
+
+            @for (consult of consultList; track consult) {
+              <ion-item button detail (click)="openConsult(consult.get('chatId'))" class="consult-item">
+                @if (consult.get('avatar')) {
+                  <ion-avatar slot="start">
+                    <img [src]="consult.get('avatar')" alt="律师头像">
+                  </ion-avatar>
+                } @else {
+                  <ion-avatar slot="start">
+                    <ion-icon name="person-circle" style="font-size: 40px;"></ion-icon>
+                  </ion-avatar>
+                }
+
+                <ion-label>
+                  <h3>{{ consult.get('name') || '未命名咨询' }}</h3>
+                  <p>{{ consult.updatedAt | date:'yyyy-MM-dd HH:mm' }}</p>
+                </ion-label>
 
-  <ion-button (click)="openConsult()" expand="block" color="primary" class="consult-btn">
-    <ion-icon name="chatbubble-ellipses-outline" slot="start"></ion-icon>
-    立即咨询
-  </ion-button>
-</ion-card>
+                <ion-badge slot="end" color="light">
+                </ion-badge>
+              </ion-item>
+            }
+          </ion-list>
+        }
+      </ion-card-content>
 
-</ion-content>
+      <ion-footer class="ion-no-border">
+        <ion-toolbar>
+          <ion-button (click)="openConsult()" expand="block" color="primary" shape="round">
+            <ion-icon slot="start" name="chatbubbles"></ion-icon>
+            开始新咨询
+          </ion-button>
+        </ion-toolbar>
+      </ion-footer>
+    </ion-card>
+  </ion-card>
+</ion-content>

+ 26 - 2
myapp/src/app/tab2/tab2.page.ts

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 import { ModalController } from '@ionic/angular/standalone';
 import { ChatPanelOptions, FmChatModalInput, FmodeChat, FmodeChatMessage, openChatPanelModal } from 'fmode-ng';
 import Parse from "parse";
+import { CloudObject, CloudQuery } from 'src/lib/ncloud';
 
 @Component({
   selector: 'app-tab2',
@@ -21,7 +22,7 @@ openConsult(chatId?:string){
     localStorage.setItem("company","E4KpGvTEto")
     let options:ChatPanelOptions = {
       roleId:"2DXJkRsjXK", // 预设,无需更改
-      // chatId:chatId, // 若存在,则恢复会话。若不存在,则开启新会话
+      chatId:chatId, // 若存在,则恢复会话。若不存在,则开启新会话
       onChatInit:(chat:FmodeChat)=>{
         console.log("onChatInit");
         console.log("Chat类",chat);
@@ -158,12 +159,35 @@ openConsult(chatId?:string){
           }
         }
       },
-      onChatSaved:(chat:FmodeChat)=>{
+      onChatSaved: async (chat:FmodeChat)=>{
         // chat?.chatSession?.id 本次会话的 chatId
         console.log("onChatSaved",chat,chat?.chatSession,chat?.chatSession?.id)
+        let chatId = chat?.chatSession?.id;
+        console.log("chatId",chatId);
+        let query = new CloudQuery("LawConsult");
+        let lawConsult = await query.get(chatId);
+        console.log("lawConsult", lawConsult);
+        if (!lawConsult?.id) { // 若无重复记录,则实例化一个新的咨询记录
+          lawConsult = new CloudObject("LawConsult")
+        }
+        lawConsult.set({
+          "chatId": chatId,
+          "messageList": chat.messageList, 
+          "name": chat.role.get("name"),
+          "avatar": chat.role.get("avatar")
+        })
+        console.log("lawConsult", lawConsult)
+        lawConsult.save();
       }
     }
     openChatPanelModal(this.modalCtrl,options)
   }
+  // 加载健康咨询记录
+  consultList: Array<CloudObject> = []
+  async loadConsult() {
+    let query = new CloudQuery("LawConsult");
+    this.consultList = await query.find()
+    console.log(this.consultList)
+  }
 
 }

+ 431 - 0
myapp/src/lib/ncloud.ts

@@ -0,0 +1,431 @@
+// CloudObject.ts
+
+let serverURL = `https://dev.fmode.cn/parse`;
+if (location.protocol == "http:") {
+    serverURL = `http://dev.fmode.cn:1337/parse`;
+}
+
+export class CloudObject {
+    className: string;
+    id: string | undefined = undefined;
+    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"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = serverURL + `/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(serverURL + `/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 = undefined;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = { where: {} };
+
+    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) {
+        if (!this.queryParams["where"]) this.queryParams["where"] = {};
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = serverURL + `/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();
+        if (json) {
+            let existsObject = this.dataToObj(json)
+            return existsObject;
+        }
+        return null
+    }
+
+    async find(): Promise<Array<CloudObject>> {
+        let url = serverURL + `/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key => {
+            let paramStr = JSON.stringify(this.queryParams[key]);
+            if (key == "include") {
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if (queryStr) {
+                url += `${key}=${paramStr}`;
+            } else {
+                url += `&${key}=${paramStr}`;
+            }
+        })
+        // if (Object.keys(this.queryParams["where"]).length) {
+
+        // }
+
+        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 = serverURL + `/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}&limit=1`;
+        }
+
+        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
+    }
+
+    dataToObj(exists: any): CloudObject {
+        let existsObject = new CloudObject(this.className);
+        Object.keys(exists).forEach(key => {
+            if (exists[key]?.__type == "Object") {
+                exists[key] = this.dataToObj(exists[key])
+            }
+        })
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}
+
+// CloudUser.ts
+export class CloudUser extends CloudObject {
+    constructor() {
+        super("_User"); // 假设用户类在Parse中是"_User"
+        // 读取用户缓存信息
+        let userCacheStr = localStorage.getItem("NCloud/dev/User")
+        if (userCacheStr) {
+            let userData = JSON.parse(userCacheStr)
+            // 设置用户信息
+            this.id = userData?.objectId;
+            this.sessionToken = userData?.sessionToken;
+            this.data = userData; // 保存用户数据
+        }
+    }
+
+    sessionToken: string | null = ""
+    /** 获取当前用户信息 */
+    async current() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return null;
+        }
+        return this;
+        // const response = await fetch(serverURL + `/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
+    }
+
+    /** 登录 */
+    async login(username: string, password: string): Promise<CloudUser | null> {
+        const response = await fetch(serverURL + `/login`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify({ username, password }),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(serverURL + `/logout`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "x-parse-session-token": this.sessionToken
+            },
+            method: "POST"
+        });
+
+        let result = await response?.json();
+
+        if (result?.error) {
+            console.error(result?.error);
+            if (result?.error == "Invalid session token") {
+                this.clearUserCache()
+                return true;
+            }
+            return false;
+        }
+
+        this.clearUserCache()
+        return true;
+    }
+    clearUserCache() {
+        // 清除用户信息
+        localStorage.removeItem("NCloud/dev/User")
+        this.id = undefined;
+        this.sessionToken = null;
+        this.data = {};
+    }
+
+    /** 注册 */
+    async signUp(username: string, password: string, additionalData: Record<string, any> = {}) {
+        const userData = {
+            username,
+            password,
+            ...additionalData // 合并额外的用户数据
+        };
+
+        const response = await fetch(serverURL + `/users`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify(userData),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+
+    override async save() {
+        let method = "POST";
+        let url = serverURL + `/users`;
+
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        let data: any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions: any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            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;
+        }
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(this.data))
+        return this;
+    }
+}
+
+export class CloudApi {
+    async fetch(path: string, body: any, options?: {
+        method: string
+        body: any
+    }) {
+
+        let reqOpts: any = {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            method: options?.method || "POST",
+            mode: "cors",
+            credentials: "omit"
+        }
+        if (body || options?.body) {
+            reqOpts.body = JSON.stringify(body || options?.body);
+            reqOpts.json = true;
+        }
+        let host = `https://dev.fmode.cn`
+        // host = `http://127.0.0.1:1337`
+        let url = `${host}/api/` + path
+        console.log(url, reqOpts)
+        const response = await fetch(url, reqOpts);
+        let json = await response.json();
+        return json
+    }
+}

+ 0 - 0
user-server/server.js