Uygulama · Veritabanı tasarımı
Veriyi tasarlamak.
İyi bir arayüz iyi bir şemanın üstünde durur. Bu sayfadaki şema fkdev.sbs, yükleme merkezi, iletişim formu ve yapay zekâ sohbeti için tek ortak yapıdır.
01Tasarım ilkeleri#
| Kural | Örnek | Neden? |
|---|---|---|
| Tablo adları çoğul, snake_case | ai_messages | Okunur ve tutarlı |
Her tabloda id BIGINT UNSIGNED | id BIGINT UNSIGNED AUTO_INCREMENT | Taşma yok, birleştirmesi kolay |
Yabancı anahtar: {tekil}_id | user_id | İlişki adından anlaşılır |
| Zaman damgası her yerde | created_at, updated_at | Hata ayıklama ve sıralama |
| Karakter seti utf8mb4 | Türkçe karakter + emoji | “ş”, “ğ” ve 🎉 bozulmaz |
| Durumlar ENUM | status ENUM('new','seen') | Geçersiz değer yazılamaz |
| Dosyalar diskte, yolu veritabanında | stored_path | Veritabanı şişmez, yedek hızlı |
Dosyayı veritabanına koymaBüyük dosyaları base64 olarak LONGTEXT içine yazmak veritabanını hızla şişirir ve paket boyutu sınırlarına takılır. Dosyayı diskte ya da nesne depolamada tut, veritabanında yalnızca yolunu ve özetini (sha256) sakla.
02ER diyagramı#
03Tablolar#
| Tablo | Amaç | Önemli alanlar |
|---|---|---|
users | Panel kullanıcıları | email benzersiz, password_hash, role |
sessions | Oturumlar (sunucusuz ortamda da çalışır) | id rastgele 64 karakter, expires_at |
projects | Portföy projeleri | slug → /projeler/{slug}/ |
uploads | Yükleme merkezi dosyaları | stored_path, sha256, status |
messages | İletişim ve İK formları | channel: contact / hr |
ai_conversations | Yapay zekâ sohbetleri | model, user_id ya da visitor_id |
ai_messages | Sohbet mesajları | role, content, token sayıları |
access_logs | Güvenlik ve erişim kayıtları | blocked, reason |
settings | Anahtar-değer ayarlar | value JSON |
04MySQL DDL#
Şemanın tamamı; boş bir veritabanında sırayla çalıştır.
-- fkdev şeması · MySQL 8 · utf8mb4
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
name VARCHAR(120) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin','editor','viewer') NOT NULL DEFAULT 'viewer',
is_active TINYINT(1) NOT NULL DEFAULT 1,
last_login_at DATETIME NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE sessions (
id CHAR(64) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
ip VARCHAR(45) NOT NULL,
user_agent VARCHAR(500) NOT NULL DEFAULT '',
expires_at DATETIME NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_sessions_user (user_id),
CONSTRAINT fk_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE projects (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(120) NOT NULL UNIQUE,
title VARCHAR(160) NOT NULL,
summary VARCHAR(500) NOT NULL DEFAULT '',
url VARCHAR(255) NULL,
status ENUM('draft','live','archived') NOT NULL DEFAULT 'draft',
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE uploads (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
project_id BIGINT UNSIGNED NULL,
original_name VARCHAR(255) NOT NULL,
stored_path VARCHAR(255) NOT NULL,
mime_type VARCHAR(128) NOT NULL,
size_bytes BIGINT UNSIGNED NOT NULL,
sha256 CHAR(64) NOT NULL,
uploader_name VARCHAR(120) NOT NULL,
uploader_email VARCHAR(190) NOT NULL,
status ENUM('new','seen','flagged') NOT NULL DEFAULT 'new',
ip VARCHAR(45) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_uploads_created (created_at),
INDEX idx_uploads_email (uploader_email),
CONSTRAINT fk_uploads_project FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel ENUM('contact','hr') NOT NULL DEFAULT 'contact',
name VARCHAR(120) NOT NULL,
email VARCHAR(190) NOT NULL,
subject VARCHAR(160) NOT NULL DEFAULT '',
body TEXT NOT NULL,
is_read TINYINT(1) NOT NULL DEFAULT 0,
ip VARCHAR(45) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_messages_unread (is_read, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE ai_conversations (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NULL,
visitor_id CHAR(36) NULL,
title VARCHAR(160) NOT NULL DEFAULT 'Yeni sohbet',
model VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ai_conv_user (user_id),
CONSTRAINT fk_ai_conv_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE ai_messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
conversation_id BIGINT UNSIGNED NOT NULL,
role ENUM('user','assistant') NOT NULL,
content MEDIUMTEXT NOT NULL,
input_tokens INT UNSIGNED NOT NULL DEFAULT 0,
output_tokens INT UNSIGNED NOT NULL DEFAULT 0,
stop_reason VARCHAR(32) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ai_msg_conv (conversation_id, id),
CONSTRAINT fk_ai_msg_conv FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE access_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NULL,
ip VARCHAR(45) NOT NULL,
method VARCHAR(10) NOT NULL,
path VARCHAR(500) NOT NULL,
status SMALLINT UNSIGNED NOT NULL,
blocked TINYINT(1) NOT NULL DEFAULT 0,
reason VARCHAR(120) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_logs_created (created_at),
INDEX idx_logs_ip (ip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE settings (
name VARCHAR(64) PRIMARY KEY,
value JSON NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;05PHP ile güvenli bağlantı (PDO)#
<?php
declare(strict_types=1);
function db(): PDO {
static $pdo = null;
if ($pdo) return $pdo;
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', getenv('DB_HOST'), getenv('DB_NAME'));
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
}<?php
$stmt = db()->prepare('SELECT id, name, role, password_hash FROM users WHERE email = ? AND is_active = 1 LIMIT 1');
$stmt->execute([$_POST['email'] ?? '']);
$user = $stmt->fetch();
if (!$user || !password_verify($_POST['password'] ?? '', $user['password_hash'])) {
http_response_code(401);
exit('E-posta ya da şifre hatalı.');
}Asla string birleştirme"... WHERE email = '$email'" gibi sorgular SQL enjeksiyonuna açıktır. Her zaman prepare() + execute() kullan.
06Sık kullanılan sorgular#
SELECT DATE(created_at) AS gun, COUNT(*) AS adet, SUM(size_bytes) AS toplam
FROM uploads
WHERE created_at >= NOW() - INTERVAL 12 DAY
GROUP BY DATE(created_at)
ORDER BY gun;SELECT COUNT(*) FROM messages WHERE is_read = 0;SELECT role, content FROM ai_messages
WHERE conversation_id = ?
ORDER BY id
LIMIT 50;07Güvenlik kontrol listesi#
- Şifreleri
password_hash()(PHP) ya da bcrypt/argon2 (Node) ile sakla; düz metin ya da MD5/SHA1 asla. - Uygulama kullanıcısına yalnızca gereken yetkileri ver (
SELECT, INSERT, UPDATE, DELETE);DROPveGRANTverme. - Veritabanı şifresini koda değil ortam değişkenine (
.env, Vercel Environment Variables) yaz;.envdosyasını depoya ekleme. - Kişisel verileri (e-posta, IP) yalnızca gerektiği kadar tut; eski log’ları düzenli sil.
- Her gece otomatik yedek al ve yedekten geri yüklemeyi gerçekten dene.
DELETE FROM access_logs WHERE created_at < NOW() - INTERVAL 90 DAY;08Şema değişiklikleri (migration)#
Canlı veritabanını elle değiştirme. Her değişikliği numaralı bir dosyaya yaz ve sırayla uygula; böylece her ortam aynı şemaya sahip olur.
migrations/
├── 001_create_users.sql
├── 002_create_uploads.sql
└── 003_add_status_to_uploads.sqlALTER TABLE uploads ADD COLUMN status ENUM('new','seen','flagged') NOT NULL DEFAULT 'new';