Bỏ qua để đến nội dung

Data model

Tất cả PK là integer auto-increment. Mọi FK trỏ c_contacts.id cũng là integer.

# Collection Loại Mô tả
1 contacts label (không có bảng) Thư mục gom nhóm — đã tồn tại sẵn trên backend, wizard không tạo
2 c_contacts table Bảng trung tâm — person + organization cùng bảng, phân biệt bằng type
3 c_contact_profiles table 1:1 hồ sơ CRM (giao tiếp, quan hệ, tính cách, mối quan tâm)
4 c_contact_social_links table O2M — platform + handle + url
5 c_contact_notes table O2M — ghi chú tự do, có author
6 c_contact_intakes table Hàng đợi triage từ nguồn ngoài
7 c_contact_settings singleton default_status, name_composition
8 c_contact_emails table O2M — nhiều email/contact, cờ is_primary
9 c_contact_phones table O2M — nhiều phone/contact, cờ is_primary
10 c_contact_addresses table O2M — nhiều địa chỉ/contact, cờ is_primary
11 c_contact_employments table Lịch sử làm việc: contact_id (person) × organization_id (org)
12 c_contact_activities table O2M nhật ký hoạt động (note/call/email/meeting)
13 c_contact_activity_types table Lookup loại hoạt động (key unique)
14 c_contact_tags_junction junction M2M c_contactsshared_tags
15 c_contact_types_junction junction M2M c_contactsshared_types

Bảng shared_* dùng chung, cũng do wizard Contacts tạo (nhưng không thuộc sở hữu riêng của module):

Collection Scope Mô tả
shared_tags contacts: Tag tự do
shared_kinds không scope person / organization — dùng chung toàn hệ thống
shared_statuses contacts: Trạng thái vòng đời contact
shared_types contacts: Loại quan hệ (customer/lead/vendor/partner/investor)

Collection dùng chung tham chiếu tới nhưng không thuộc Contacts:

  • odp_userstriaged_by (intake), author_id (activity)

FK ordering — vì sao thứ tự này bắt buộc

Phần tiêu đề “FK ordering — vì sao thứ tự này bắt buộc”

Setup Wizard tạo theo sort khai báo trong contacts-schema.ts: c_contacts trước tiên (không phụ thuộc gì), rồi mọi bảng O2M phụ thuộc nó (profiles, social_links, notes, emails, phones, addresses, activities), rồi employments (phụ thuộc c_contacts hai lần — cả contact_id lẫn organization_id), rồi 2 junction M2M, cuối cùng 4 bảng shared_*.

  • Thư mụcadmin/packages/contacts/
    • app/data/contacts-schema.ts ModuleSchema — collections + fields + relations + seed, chạy qua Setup Wizard
Field Type Ghi chú
type string, required person | organization
display_name string, required Tên hiển thị — tự ghép từ first_name+last_name nếu để trống (hook items.create/items.update)
primary_email, primary_phone string Đồng bộ 2 chiều với bản ghi is_primaryc_contact_emails/phones — xem detail-sync.ts
organization_id, position integer, string Denormalize từ employment is_current: true — backend hook tự tính lại, form để readonly khi sửa
types alias, special: ["m2m"] Join qua c_contact_types_junction
tags alias, special: ["m2m"] Join qua c_contact_tags_junction
metadata json, hidden Payload mở rộng (chưa dùng rộng rãi)

3 bảng O2M (c_contact_emails, c_contact_phones, c_contact_addresses) cho phép nhiều bản ghi/contact với type (work/personal/billing…) và cờ is_primary. detail-sync.ts là nơi giữ đồng bộ:

backend/extensions/contacts/src/endpoints/services/detail-sync.ts
export async function syncPrimaryEmail(ctx: AppContext, contactId: number, email: string) {
await syncPrimaryDetail(ctx, 'c_contact_emails', 'address', contactId, email);
}
  • Create: createPrimaryRecords() — nếu body có primary_email/primary_phone, tạo luôn bản ghi con is_primary: true.
  • Update: syncPrimaryEmail/syncPrimaryPhone/syncPrimaryAddress — tìm bản ghi is_primary: true hiện có; có thì update, chưa có thì tạo mới (chỉ khi giá trị không rỗng).

c_contact_employments — quan hệ person ↔ organization

Phần tiêu đề “c_contact_employments — quan hệ person ↔ organization”
Field Ghi chú
contact_id M2O → c_contacts (person)
organization_id M2O → c_contacts (organization) — cùng bảng, phân biệt bằng vai trò field
is_current boolean — chỉ một true tại một thời điểm cho mỗi contact_id

Hook syncEmployment() enforce “chỉ một current” bằng raw Knex updateMany (cố ý không dùng ItemsService.updateMany — sẽ re-emit items.update và đệ quy vào chính hook này).

Xoá một contact xoá theo mọi bảng con trước khi xoá hàng c_contacts chính (cascade-delete.ts):

backend/extensions/contacts/src/endpoints/services/cascade-delete.ts
const CHILD_COLLECTIONS = [
'c_contact_emails', 'c_contact_phones', 'c_contact_addresses',
'c_contact_activities', 'c_contact_social_links', 'c_contact_profiles',
'c_contact_notes', 'c_contact_tags_junction', 'c_contact_types_junction',
];

c_contact_employments được xử lý riêng vì cả hai cột contact_idorganization_id đều NOT NULL — xoá contact đang là employer của ai đó cũng phải xoá luôn employment (không thể set null).

Junction Nối Field alias tương ứng
c_contact_tags_junction c_contactsshared_tags c_contacts.tags
c_contact_types_junction c_contactsshared_types c_contacts.types

m2m-hydrator.ts đọc cả hai bằng một query whereIn mỗi junction (không N+1):

backend/extensions/contacts/src/endpoints/services/m2m-hydrator.ts
const [tagRows, typeRows] = await Promise.all([
ctx.database('c_contact_tags_junction').whereIn('c_contacts_id', ids)
.select('c_contacts_id', 'shared_tags_id'),
ctx.database('c_contact_types_junction')
.join('shared_types', 'c_contact_types_junction.shared_types_id', 'shared_types.id')
.whereIn('c_contacts_id', ids)
.select('c_contacts_id', 'shared_types.slug as shared_types_slug'),
]);

Ghi (create/update contact) đi qua resolveTagsForWrite/resolveTypesForWrite — nhận string (tag name/slug hoặc type slug) từ client, resolve/tạo mới hàng shared_* tương ứng, rồi trả về mảng { shared_tags_id }/{ shared_types_id } cho ItemsService tự M2M-sync (khác với Helpdesk, ở đây để ItemsService sync junction, vì input luôn là toàn bộ tập đích chứ không phải patch một phần).