3 · Persistence & integrations — chat-center-api
อ้างอิง branch: prod
Data stores overview
| Store | Connection name | Database | Host (จาก .env) | synchronize | หลักฐาน |
|---|---|---|---|---|---|
| MSSQL | main | 2way | 10.188.128.61:1433 | true | app.module.ts:33-38, configuration.ts:7-21, .env:7-12 |
| MSSQL | center | dbHRMI_Center | 10.188.128.61:1433 | false | app.module.ts:39-44, configuration.ts:22-37, .env:14-19 |
| MongoDB | (Mongoose default) | chatDB | 10.188.128.64:27017 (จาก MONGODB URI) | n/a | app.module.ts:45-51, .env:26 |
| Local disk | — | ./uploads/{YYYY-MM-DD}/ | container filesystem | n/a | chat.gateway.ts:82-87 |
NODE_ENV=uat ใน .env ที่ commit อยู่ (chat-center-api/.env (branch: prod):1) ทั้งที่ SERVER_URL ชี้ production API host — ค่า env ผสมระหว่าง UAT/prod ในไฟล์เดียว
Config ของ main
db: {
type: 'mssql',
host: process.env.DB_HOST,
// ...
database: process.env.DB_NAME,
synchronize: true,
logging: [`error`, `query`],
entities: [__dirname + '/../src/entities/*.entity{.ts,.js}'],
extra: { trustServerCertificate: true, encrypt: false },
}หลักฐาน — chat-center-api/config/configuration.ts (branch: prod):7-21
synchronize: true ใน environment ที่ชี้ IP production = ความเสี่ยง schema drift / data loss (CORR-CHAT-02)
Config ของ center
- อ่านจาก
DB_HRMI_* autoLoadEntities: false,synchronize: falseentitiespath ชี้process.cwd() + '/dist/**/*.entity{.ts,.js}'— แต่โค้ดใช้entityManagerCenter.query()raw SQL เป็นหลัก ไม่พึ่ง entity ของ center
หลักฐาน — configuration.ts:22-37
MSSQL entities (main / database 2way)
ทุก entity extend BaseEntity:
| Column | Type | Default | หลักฐาน |
|---|---|---|---|
id | increment PK | — | base.ts:4-5 |
createdAt | datetime | CURRENT_TIMESTAMP | base.ts:7-8 |
createdBy | varchar(50) | 'SYSTEM ADMIN' | base.ts:10-11 |
updatedAt | datetime | CURRENT_TIMESTAMP | base.ts:13-14 |
updatedBy | varchar(50) | 'SYSTEM ADMIN' | base.ts:16-17 |
chatRoom — ChatRoomEntity
ตารางหลักของห้องแชท — unique (empCode, sourceDB)
| Column | หมายเหตุ | หลักฐาน |
|---|---|---|
empCode, identityCard, fullName | ข้อมูลพนักงานจาก HRMI ตอนสร้างห้อง | chatRoom.entity.ts:9-16 |
orgUnitCode, orgName, position*, divi*, dept* | โครงสร้างองค์กร | chatRoom.entity.ts:18-42 |
sourceDB | tenant key ข้ามหน่วยงาน (default 'test') | chatRoom.entity.ts:44-45 |
tagsId | JSON string ของ array id เช่น '["1","2"]' — ไม่ใช่ relation | chatRoom.entity.ts:47-48 |
careTakerName, careTakerEmpCode | ผู้ดูแลปัจจุบัน | chatRoom.entity.ts:50-54 |
noteTag | JSON string ของ empCode ที่ถูกแท็กในโน้ต | chatRoom.entity.ts:56-57 |
replyToken | LINE user ID (ใช้เป็น emit room key) | chatRoom.entity.ts:59-60 |
status | 'w' / 'p' / 's' (default 'w') | chatRoom.entity.ts:62-64 |
phoneNumber | จาก HRMI | chatRoom.entity.ts:66-67 |
isRead | มีข้อความใหม่ที่ยังไม่อ่าน | chatRoom.entity.ts:69-70 |
isChat | ห้องเปิดรับข้อความทั่วไปจาก LINE หรือยัง | chatRoom.entity.ts:72-73 |
หลักฐาน entity — chat-center-api/src/entities/chatRoom.entity.ts (branch: prod):6-77
ตาราง userLine ที่ถูก query ใน webhook/gateway (SELECT * FROM userLine WHERE lineID = @0) ไม่มี TypeORM entity ใน repo — เป็นตารางใน schema 2way ที่โค้ดเข้าถึงแบบ raw SQL เท่านั้น
chatRoomStatusHistory — ChatRoomStatusHistoryEntity
| Column | หมายเหตุ | หลักฐาน |
|---|---|---|
chatRoomId | FK ไป chatRoom | chatRoomStatusHistory.entity.ts:13-14 |
status | 'w' / 'p' / 's' | chatRoomStatusHistory.entity.ts:16-18 |
chatRoom | ManyToOne relation | chatRoomStatusHistory.entity.ts:9-11 |
teamAdmin / teamMember / teamTags / tags
โมเดลสิทธิ์:
TeamAdminEntity (level: 1 = super admin, default 2)
├── members: TeamMemberEntity[] (empCode, fullName)
└── tags: TeamsTagsEntity[] (tagsId → TagsEntity)
└── TagsEntity (tagName, tagColor, isShow, isDelete)| Entity | Unique | ฟิลด์สำคัญ | หลักฐาน |
|---|---|---|---|
TeamAdminEntity | (teamName, sourceDB) | level, isShow, isDelete | teamAdmin.entity.ts:6-32 |
TeamMemberEntity | — | empCode, fullName, FK teamAdminId | teamMember.entity.ts:5-16 |
TeamsTagsEntity | — | tagsId, FK teamAdminId | teamTags.entity.ts:24-41 |
TagsEntity | (tagName, sourceDB) | tagName, tagColor, isShow | tags.entity.ts:5-25 |
Super admin สร้างผ่าน POST /teamAdmin/onlySuperAdmin ที่บังคับ level = 1 และ tags = [] (teamadmin.service.ts:118-134)
appointment — AppointmentEntity
| Column | หมายเหตุ | หลักฐาน |
|---|---|---|
empCodeRoom | เจ้าของห้องที่นัด | appointment.entity.ts:9-10 |
title, description | appointment.entity.ts:12-16 | |
appointmentDate | เวลานัด | appointment.entity.ts:18-19 |
timeDiff, timeUnit | ใช้คำนวณเวลาแจ้งเตือนล่วงหน้า | appointment.entity.ts:24-28 |
sourceDB, isDelete | soft delete | appointment.entity.ts:30-34 |
ไม่มี column เก็บ CronJob id หรือสถานะว่าแจ้งเตือนแล้วหรือยัง — reminder state อยู่ใน memory เท่านั้น
notifications — NotificationsEntity
| Column | หมายเหตุ | หลักฐาน |
|---|---|---|
empCodeRoom | ห้องที่เกี่ยวข้อง | notifications.entity.ts:9-10 |
message | ข้อความแจ้งเตือน | notifications.entity.ts:12-13 |
adminName, adminEmpCode | ผู้รับ (care taker) | notifications.entity.ts:15-19 |
superAdmin | ถ้า true แจ้งกลุ่ม super admin | notifications.entity.ts:21-22 |
isRead | notifications.entity.ts:27-28 |
listNoti แยก query ตามว่า user เป็นสมาชิกทีม level: 1 หรือไม่ (chat.service.ts:283-324) — ใช้ QueryBuilder parameterized (ปลอดภัยกว่า rooms())
satisfactionSurvey — SatisfactionSurveyEntity
| Column | หมายเหตุ | หลักฐาน |
|---|---|---|
key | unique, default NEWID() — ใช้ใน URL feedback | satisfactionSurvey.entity.ts:8-11 |
roomEmpCode, roomName | สำเนาจากห้องตอนปิดสถานะ s | satisfactionSurvey.entity.ts:13-17 |
score, description | คะแนนจากผู้ตอบ (@Public save) | satisfactionSurvey.entity.ts:19-23 |
isAction | true เมื่อตอบแล้ว | satisfactionSurvey.entity.ts:28-29 |
careTaker*, tagsId, sourceDB | snapshot ตอนสร้าง | entity ทั้งไฟล์ |
genSurvey สร้างแถวเปล่าแล้วคืน URL UAT (satisfedurvey.service.ts:89-97) — path front hardcode เหมือนตอนปิดห้อง
MongoDB schemas (chatDB)
Conversation
| Field | ใช้เมื่อ | หลักฐาน |
|---|---|---|
empCode | userID จาก userLine | conversation.schema.ts:8-9 |
sourceDB | จาก HRMI profile | conversation.schema.ts:11-12 |
userActionMessage / userActionTime | ข้อความจาก LINE/user | conversation.schema.ts:14-18 |
adminActionMessage / adminActionTime / adminActionEmpCode | ข้อความจากแอดมิน/bot | conversation.schema.ts:20-27 |
type | 'text' หรือ 'file' | conversation.schema.ts:29-30 |
replyToken | LINE user ID | conversation.schema.ts:32-33 |
createdAt | ใช้ทั้ง sort และ TTL delete 90 วัน | conversation.schema.ts:35-36 |
เอกสารหนึ่งแถว = หนึ่งทิศทางของข้อความ (user หรือ admin) — ไม่ใช่ thread document เดียว
ChatNote
| Field | หมายเหตุ | หลักฐาน |
|---|---|---|
roomEmpCode | ห้อง | chatNote.schema.ts:8-9 |
adminEmpCode, adminFullName | ผู้เขียนโน้ต | chatNote.schema.ts:11-15 |
message | เนื้อหา | chatNote.schema.ts:17-18 |
tagEmpCode, tagFullName | คนที่ถูกแท็ก (optional) | chatNote.schema.ts:20-24 |
createdAt | chatNote.schema.ts:26-27 |
โน้ตไม่ถูก push ไป LINE — เห็นเฉพาะใน backoffice ผ่าน chatRoomHistory
Cross-database reads (center / HRMI)
จุดที่โค้ด query เข้า dbHRMI_Center โดยตรง:
| ตำแหน่ง | SQL / ตาราง | หลักฐาน |
|---|---|---|
| Webhook โหลดโปรไฟล์ | temp_pee_na_table WHERE EmpCode | app.service.ts:38-40 |
| Gateway แอดมินตอบ | เหมือนกัน | chat.gateway.ts:70-72 |
imageProfile | vw_EmployeeProfileWithImage | app.service.ts:265-267 |
imageProfileMulti | vw_EmployeeProfileWithImage + SourceDB interpolate | app.service.ts:277-281 |
listMember | temp_pee_na_table pagination OFFSET/FETCH | teamadmin.service.ts:82-85 |
roomStatusHistory | temp_pee_na_table เพื่อประกอบ fullName | chat.service.ts:350-351 |
imageProfileMulti bind empCodes ด้วย @0,@1,... แต่ยัง interpolate sourceDB จาก JWT:
`... and SourceDB = '${user.profile.sourceDB}' `หลักฐาน — chat-center-api/src/app.service.ts (branch: prod):277-281
ถ้า JWT ถูก forge (ดู SEC-CHAT-04 เรื่อง SECRET_KEY หลุด) ค่า sourceDB ใน token กลายเป็น injection vector ได้อีกจุด
External integrations
| ปลายทาง | ทิศทาง | Endpoint / path | Auth | หลักฐาน |
|---|---|---|---|---|
| LINE Messaging API | ออก | https://api.line.me/v2/bot/message/push | Bearer LINE_AUTH | configuration.ts:42-44, chat.gateway.ts:105,148,194 |
| LINE Platform | เข้า | POST .../webhook | @Public() — ไม่ verify signature ในโค้ด | app.controller.ts:18-22 |
| 2way-meeting-backoffice | เข้า (caller หลัก) | REST + Socket.IO | JWT / ไม่มีบน WS | ดู .env ของ backoffice (ข้าม repo) |
| HRMI MSSQL | อ่านตรง | connection center | DB credential | ตารางด้านบน |
| cu-Gateway (dead) | ออก (comment ออกแล้ว) | http://20.212.116.206:8200/api/v1/line | — | chat.gateway.ts:114-115 |
Config LINE ที่โหลด:
lineChannelSecret: process.env.LINE_CHANNEL_SECRET,
lineAuth: process.env.LINE_AUTH,
lineChannel: process.env.LINE_CHANNEL,
lineApi: { pushMessage: 'https://api.line.me/v2/bot/message/push' },หลักฐาน — configuration.ts:39-44
ในโค้ด runtime ที่อ่านได้ — lineAuth ถูกใช้เป็น Bearer ตอน push; lineChannelSecret ถูกโหลดเข้า config แต่ ไม่พบ การนำไป verify webhook signature
CENTRALIZE_SERVER ใน .env — ไม่พบ reference ใน src/ หรือ libs/ (vestigial)
File storage
| Path | ใครเขียน | Serve อย่างไร |
|---|---|---|
./uploads/{YYYY-MM-DD}/{random}.{ext} | ChatGateway เมื่อมี base64 | useStaticAssets prefix /chat-center-api/ (main.ts:28-30) |
URL ที่ส่งให้ LINE เป็น text ของ full URL ผ่าน hostServer ไม่ใช่ LINE content API upload — ผู้รับต้องเปิดลิงก์ HTTP ไปที่ static file ของ chat-center-api
ถ้า container ไม่ mount volume — ไฟล์หายเมื่อ redeploy (OBS-CHAT-02)
Swagger
docs/document.config.ts ตั้ง title "Vtrc RestFull" และเพิ่ม server สามตัว:
SERVER_URLจาก envhttp://localhost:{PORT||8200}- ngrok URL hardcode
https://3fe5-49-228-70-244.ngrok-free.app
หลักฐาน — chat-center-api/docs/document.config.ts (branch: prod):3-11
ngrok URL ที่ commit อาจหมดอายุแล้ว — เป็น leftover จาก dev tunnel
Tenant model (sourceDB)
เกือบทุกตารางและ query กรองด้วย sourceDB จาก user.profile.sourceDB ใน JWT — เป็น soft multi-tenant ภายใน database เดียว (2way / chatDB) ไม่ใช่ database แยกต่อหน่วยงาน
Super admin level === 1 ยังถูกกรอง sourceDB เหมือนกันใน rooms() — เห็นทุกห้องใน sourceDB เดียวกัน ไม่ใช่ข้ามทุก tenant