Modelagem de Banco de Dados de um Marketplace de Carros Usados: Tabelas, Dados de Exemplo, Transações e Maquetes do App, 100 Exemplos do Zero ao Profissional
Um marketplace de carros usados conecta compradores, vendedores e revendas em torno de um bem caro, único e que precisa ser transferido legalmente. Todo carro tem um número de chassi (VIN), um histórico de quilometragem, um laudo de vistoria e um documento de registro que precisa mudar de dono corretamente. Este artigo modela o banco de dados de um marketplace assim, do esquema vazio até uma versão pensada para produção, em 100 exemplos. Você vai listar as regras de negócio, criar as tabelas de usuários e revendas, um catálogo de carros com marcas, modelos e versões, anúncios com fotos, vistorias e leituras de quilometragem, índices de busca, ofertas e contraofertas, pedidos com um fluxo de status claro, pagamentos registrados como livro-razão, as etapas da transferência de propriedade com documentos, financiamento e garantias, avaliações e denúncias de fraude, e as transações que mantêm tudo consistente sob uso concorrente. Cada parte inclui dados de exemplo, o que acontece dentro da transação quando ela roda, e uma maquete da tela do app que a utiliza. Nota de transparência: o MySQL não estava instalado no computador usado para preparar este artigo, então nenhum dos 100 exemplos foi executado aqui. O SQL segue a sintaxe do MySQL 8.4, mas execute cada parte você mesmo em um banco de testes antes de confiar nela.
O que você precisa
MySQL 8.4 (ou Docker para executá-lo em um contêiner), um cliente SQL como o MySQL Workbench ou o utilitário de linha de comando mysql, e entendimento básico de tabelas, chaves e junções. Use nomes de pessoas, e-mails e VINs fictícios, nunca dados reais de clientes, e nunca cole senhas, chaves de API ou dados bancários reais nos scripts.
Como usar um exemplo
Os exemplos em SQL rodam no MySQL 8.4 em um banco de testes, na ordem indicada. Os exemplos em HTML são maquetes de tela que abrem no navegador. Os exemplos em JSON mostram o formato dos dados da API, e os exemplos em bash usam os comandos mysql e curl.
Tópico 1: Regras de Negócio e Configuração
Comece pelas regras, não pelas tabelas. Quem pode anunciar um carro, quem pode comprá-lo, o que precisa ser verdade antes de o anúncio entrar no ar e o que nunca pode acontecer, como vender o mesmo carro duas vezes. O banco de dados impõe essas regras para que um erro na aplicação não consiga quebrá-las.
Atores e o que precisam
Compradores pesquisam, salvam favoritos e fazem ofertas. Vendedores anunciam carros e respondem às ofertas. Revendas anunciam muitos carros e têm um selo de verificação. Inspetores avaliam carros, e administradores analisam denúncias. Cada papel precisa de suas próprias permissões e telas.
Regras que o banco de dados precisa garantir
Um carro só pode ser vendido a um comprador por vez. Um anúncio só vai ao ar depois de uma vistoria aprovada. O sinal fica retido até a transferência de propriedade ser concluída. Toda mudança de preço e de status é registrada. Escreva essas regras primeiro e depois decida quais serão impostas por restrições CHECK, chaves estrangeiras e transações.
Conjunto de caracteres e banco de dados
Crie o banco com utf8mb4 para que textos em tailandês, emojis e nomes sejam guardados exatamente como digitados. Use o mesmo conjunto de caracteres em todas as tabelas, porque codificações misturadas causam textos corrompidos e erros de comparação.
Ilustrações
Exemplo 1: Escreva primeiro as regras do negócio
Quem faz o quê e quais regras o banco deve garantir.
{
"buyers": "search, favorite, make an offer, pay a deposit, buy",
"sellers": "list a car with photos and a price, accept or reject offers",
"dealers": "list many cars, get a verified badge after a document check",
"rules": [
"a car can be sold to one buyer only",
"a deposit is held until the ownership transfer is done",
"every price change and status change is recorded"
]
}
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 2: Criar o banco de dados
Use utf8mb4 para guardar corretamente texto em tailandês e emojis.
CREATE DATABASE IF NOT EXISTS carmarket
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE carmarket;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 3: Uma tabela de usuários
Uma tabela de contas para compradores, vendedores, revendas e equipe.
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
phone VARCHAR(20) NULL,
password_hash CHAR(60) NOT NULL,
full_name VARCHAR(120) NOT NULL,
role ENUM('buyer','seller','dealer','inspector','admin') NOT NULL DEFAULT 'buyer',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 4: Usuários de exemplo
Contas fictícias com hashes de senha de exemplo.
INSERT INTO users (email, phone, password_hash, full_name, role) VALUES
('[email protected]', '0810000001', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Somchai Example', 'seller'),
('[email protected]', '0810000002', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Malee Example', 'buyer'),
('[email protected]','0810000003', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Prime Auto Example', 'dealer'),
('[email protected]','0810000004','$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Inspector Example', 'inspector');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 5: Uma tabela de revendas
Dados da empresa separados da conta de login.
CREATE TABLE dealers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL UNIQUE,
business_name VARCHAR(160) NOT NULL,
tax_id CHAR(13) NOT NULL UNIQUE,
is_verified TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 6: Uma revenda de exemplo
Vincule uma empresa verificada à sua conta de usuário.
INSERT INTO dealers (user_id, business_name, tax_id, is_verified)
SELECT id, 'Prime Auto Example Co., Ltd.', '0105500000001', 1
FROM users WHERE email = '[email protected]';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 2: Usuários, Revendas e Endereços
Todo mundo que faz login tem uma linha em users, com um papel. Revendas guardam dados empresariais extras na própria tabela, e os endereços permitem que a busca filtre carros por província. Separar essas tabelas mantém cada uma pequena e cada regra fácil de verificar.
Uma tabela de contas com papéis
A tabela users guarda o e-mail, o hash da senha e o papel. Um papel como vendedor, comprador, revenda, inspetor ou administrador define quais telas e ações são permitidas. Nunca armazene senhas em texto puro. Guarde apenas o hash bcrypt.
Revendas como tabela separada
Uma revenda é um usuário com empresa registrada, então o nome empresarial e o CNPJ ficam em dealers e apontam para users por chave estrangeira. A flag is_verified só é ativada depois que a equipe confere os documentos, e só revendas verificadas exibem o selo.
Endereços e preferências
A província é guardada em cada endereço e em cada anúncio, com um índice para que a busca por uma província seja rápida. As preferências do comprador, como orçamento e tipo de combustível, ficam separadas para poderem mudar sem tocar na conta.
Exemplo 7: Endereços
Guarde os locais para a busca filtrar por província.
CREATE TABLE addresses (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
line1 VARCHAR(200) NOT NULL,
district VARCHAR(100) NOT NULL,
province VARCHAR(100) NOT NULL,
postal_code CHAR(5) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_addresses_province (province)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 8: Um endereço de exemplo
Endereço de um vendedor em Chonburi.
INSERT INTO addresses (user_id, line1, district, province, postal_code)
SELECT id, '99/1 Example Road', 'Mueang', 'Chonburi', '20000'
FROM users WHERE email = '[email protected]';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 9: Preferências do comprador
Guarde o orçamento e o tipo de combustível preferido.
CREATE TABLE buyer_preferences (
user_id BIGINT UNSIGNED PRIMARY KEY,
max_budget DECIMAL(12,2) NULL,
preferred_fuel ENUM('petrol','diesel','hybrid','ev','lpg','cng') NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 10: Sessões de login
Faça as sessões expirarem e apague-as junto com o usuário.
CREATE TABLE sessions (
id CHAR(64) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_sessions_expires (expires_at)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 11: Marcas de carros
Uma linha por marca, com nome único.
CREATE TABLE car_makes (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(60) NOT NULL UNIQUE
) ENGINE=InnoDB;
INSERT INTO car_makes (name) VALUES ('Toyota'), ('Honda'), ('Mazda'), ('Isuzu'), ('Mitsubishi');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 12: Modelos de carros
Cada modelo pertence a exatamente uma marca.
CREATE TABLE car_models (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
make_id SMALLINT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
body_type ENUM('sedan','hatchback','suv','pickup','van') NOT NULL,
UNIQUE KEY uq_model (make_id, name),
FOREIGN KEY (make_id) REFERENCES car_makes(id)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 13: Modelos de exemplo
Insira modelos com junção à tabela de marcas.
INSERT INTO car_models (make_id, name, body_type)
SELECT m.id, x.name, x.body
FROM car_makes m
JOIN (SELECT 'Toyota' AS make, 'Camry' AS name, 'sedan' AS body
UNION ALL SELECT 'Toyota', 'Yaris Ativ', 'sedan'
UNION ALL SELECT 'Honda', 'City', 'sedan'
UNION ALL SELECT 'Honda', 'CR-V', 'suv'
UNION ALL SELECT 'Mazda', 'CX-5', 'suv'
UNION ALL SELECT 'Isuzu', 'D-Max', 'pickup') x ON x.make = m.name;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 14: Versões (trims)
O mesmo modelo vem em versões com motores diferentes.
CREATE TABLE car_trims (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
model_id INT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
year_from SMALLINT UNSIGNED NOT NULL,
year_to SMALLINT UNSIGNED NULL,
engine_cc SMALLINT UNSIGNED NOT NULL,
fuel ENUM('petrol','diesel','hybrid','ev','lpg','cng') NOT NULL,
transmission ENUM('auto','manual','cvt') NOT NULL,
seats TINYINT UNSIGNED NOT NULL DEFAULT 5,
FOREIGN KEY (model_id) REFERENCES car_models(id),
INDEX idx_trims_model_fuel (model_id, fuel)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 3: O Catálogo de Carros: Marcas, Modelos e Versões
Um catálogo guarda os fatos sobre um modelo uma única vez, em vez de repeti-los em cada anúncio. Uma marca tem muitos modelos, um modelo tem muitas versões, e uma versão tem o motor, o combustível e o câmbio que o comprador realmente compara.
Marca, modelo, versão
Uma marca como Toyota tem modelos como o Camry. Uma versão como G 2.5 é uma variante desse modelo com motor e câmbio próprios. O mesmo modelo pode ter várias versões em anos diferentes, então as faixas de ano pertencem à versão.
Opcionais e relações muitos para muitos
Opcionais como teto solar ou câmera de ré não existem em todas as versões. Uma tabela de ligação conecta opcionais a versões, então um mesmo opcional pode aparecer em várias versões e uma versão pode ter vários opcionais.
Por que o catálogo importa
Quando o vendedor escolhe uma versão, o motor, o combustível e o número de lugares vêm do catálogo. O vendedor não consegue digitá-los errado, e a busca por combustível ou câmbio funciona com confiança, porque os valores são guardados como códigos, não como texto livre.
Exemplo 15: Uma versão do catálogo
Versão a gasolina automática para uma faixa de anos.
INSERT INTO car_trims (model_id, name, year_from, year_to, engine_cc, fuel, transmission)
SELECT id, 'G 2.5', 2018, 2023, 2487, 'petrol', 'auto'
FROM car_models WHERE name = 'Camry';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 16: Uma versão híbrida
Versão híbrida com câmbio CVT e sem ano final.
INSERT INTO car_trims (model_id, name, year_from, year_to, engine_cc, fuel, transmission)
SELECT id, 'Hybrid Premium', 2019, NULL, 1798, 'hybrid', 'cvt'
FROM car_models WHERE name = 'Camry';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 17: Opcionais
Teto solar, câmera, assistente de faixa e bancos de couro como códigos.
CREATE TABLE car_options (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(40) NOT NULL UNIQUE,
label VARCHAR(80) NOT NULL
) ENGINE=InnoDB;
INSERT INTO car_options (code, label) VALUES
('sunroof', 'Sunroof'), ('rear_camera', 'Rear camera'), ('lane_assist', 'Lane assist'), ('leather', 'Leather seats');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 18: Opcionais por versão
Vincule os opcionais às versões com uma tabela muitos para muitos.
CREATE TABLE trim_options (
trim_id INT UNSIGNED NOT NULL,
option_id SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (trim_id, option_id),
FOREIGN KEY (trim_id) REFERENCES car_trims(id) ON DELETE CASCADE,
FOREIGN KEY (option_id) REFERENCES car_options(id)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 19: Anúncios de venda
Uma linha por carro físico à venda, com regras CHECK.
CREATE TABLE car_listings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
seller_id BIGINT UNSIGNED NOT NULL,
trim_id INT UNSIGNED NOT NULL,
vin CHAR(17) NOT NULL UNIQUE,
plate VARCHAR(20) NULL,
year SMALLINT UNSIGNED NOT NULL,
mileage_km INT UNSIGNED NOT NULL,
color VARCHAR(40) NOT NULL,
asking_price DECIMAL(12,2) NOT NULL,
status ENUM('draft','pending_review','active','reserved','sold','removed') NOT NULL DEFAULT 'draft',
province VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (seller_id) REFERENCES users(id),
FOREIGN KEY (trim_id) REFERENCES car_trims(id),
CHECK (asking_price > 0),
CHECK (mileage_km >= 0)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 20: Anúncio ativo de exemplo
Um Camry 2019 à venda em Chonburi.
INSERT INTO car_listings (seller_id, trim_id, vin, plate, year, mileage_km, color, asking_price, status, province)
SELECT u.id, t.id, 'JTNB11HK103000001', 'กข 1234', 2019, 48250, 'White', 765000.00, 'active', 'Chonburi'
FROM users u, car_trims t
WHERE u.email = '[email protected]' AND t.name = 'G 2.5' AND t.year_from = 2018
LIMIT 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 21: Fotos do carro
Guarde as URLs das fotos com uma ordem de exibição fixa.
CREATE TABLE car_images (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
url VARCHAR(500) NOT NULL,
position TINYINT UNSIGNED NOT NULL DEFAULT 0,
FOREIGN KEY (listing_id) REFERENCES car_listings(id) ON DELETE CASCADE,
UNIQUE KEY uq_image_position (listing_id, position)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 22: Fotos de exemplo
Três fotos do primeiro anúncio.
INSERT INTO car_images (listing_id, url, position) VALUES
(1, 'https://cdn.example.com/cars/1/front.jpg', 0),
(1, 'https://cdn.example.com/cars/1/side.jpg', 1),
(1, 'https://cdn.example.com/cars/1/interior.jpg', 2);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 4: Anúncios e Fotos
Um anúncio é um carro físico à venda. Ele tem seu próprio VIN, quilometragem, cor, preço e status. As fotos ficam em uma tabela separada com ordem fixa, então a primeira foto é sempre a dianteira e a galeria carrega na mesma ordem toda vez.
Uma linha por carro físico
Cada carro tem um VIN único, então o mesmo carro não pode ser anunciado duas vezes. O status passa de rascunho para aguardando análise, depois para ativo, reservado, vendido ou removido. Regras CHECK impedem que um preço ou uma quilometragem negativos sejam guardados.
Alterar um anúncio com segurança
Só o vendedor pode alterar o preço. Coloque a verificação do vendedor na mesma instrução UPDATE que faz a mudança, para que não exista uma brecha entre a verificação e a gravação. Mude o status apenas pelas etapas permitidas e registre cada alteração.
Fotos e a página de detalhes
Guarde a URL da foto e a posição, não o arquivo de imagem. A página de detalhes carrega o carro, o vendedor e as fotos em uma única consulta, e a maquete mostra como o resultado se organiza no celular e no desktop.
Exemplo 23: Anúncio em rascunho
O vendedor cria o carro antes de publicá-lo.
INSERT INTO car_listings (seller_id, trim_id, vin, year, mileage_km, color, asking_price, status, province)
SELECT u.id, t.id, 'JTNB11HK103000002', 2020, 31000, 'Silver', 720000.00, 'draft', 'Bangkok'
FROM users u, car_trims t
WHERE u.email = '[email protected]' AND t.name = 'G 2.5'
LIMIT 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 24: Mudar o preço como dono
Verifique o vendedor na mesma instrução.
UPDATE car_listings
SET asking_price = 745000.00
WHERE id = 2 AND seller_id = (SELECT id FROM users WHERE email = '[email protected]');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 25: Avançar o status do anúncio
Permita a mudança de status apenas a partir da etapa anterior.
UPDATE car_listings
SET status = 'pending_review'
WHERE id = 2 AND status = 'draft';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 26: Anúncios ativos em uma província
Junte o catálogo para mostrar marca, modelo e versão.
SELECT l.id, l.year, l.mileage_km, l.asking_price, t.name AS trim, m.name AS model, mk.name AS make
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
JOIN car_models m ON m.id = t.model_id
JOIN car_makes mk ON mk.id = m.make_id
WHERE l.status = 'active' AND l.province = 'Chonburi'
ORDER BY l.asking_price;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 27: A página de detalhes do anúncio
Carregue o carro, as fotos e o vendedor em uma consulta.
SELECT l.*, u.full_name AS seller_name, GROUP_CONCAT(i.url ORDER BY i.position) AS photos
FROM car_listings l
JOIN users u ON u.id = l.seller_id
LEFT JOIN car_images i ON i.listing_id = l.id
WHERE l.id = 1
GROUP BY l.id;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 28: Maquete: o cartão de resultado
Cartão do carro com preço, especificações e botão de oferta.
<article class="car-card">
<img src="https://cdn.example.com/cars/1/front.jpg" alt="Toyota Camry 2019 white">
<h3>Toyota Camry G 2.5 (2019)</h3>
<p class="meta">48,250 km · Auto · Petrol · Chonburi</p>
<p class="price">฿765,000</p>
<button>Make an offer</button>
<button class="icon" aria-label="Save to favorites">♡</button>
</article>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 29: Maquete: a página de detalhes
Galeria, resumo e ações em uma só tela.
<main class="detail">
<section class="gallery">
<img src="front.jpg" alt="Front view">
<div class="thumbs"><img src="side.jpg" alt="Side view"><img src="interior.jpg" alt="Interior"></div>
</section>
<aside class="summary">
<h1>Toyota Camry G 2.5 (2019)</h1>
<p class="price">฿765,000</p>
<ul>
<li>Mileage: 48,250 km</li>
<li>Color: White</li>
<li>Province: Chonburi</li>
<li>Inspection score: <strong>4.5 / 5</strong></li>
</ul>
<button class="primary">Make an offer</button>
<button>Ask the seller</button>
</aside>
</main>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 30: Maquete: cores e espaçamento
Tokens de design como variáveis CSS.
<style>
:root {
--brand: #1f5fbf;
--ink: #1b1f2a;
--muted: #6b7280;
--radius: 12px;
--gap: 16px;
}
.car-card { border: 1px solid #e5e7eb; border-radius: var(--radius); padding: var(--gap); }
.price { color: var(--brand); font-size: 1.25rem; font-weight: 700; }
.meta { color: var(--muted); }
</style>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 5: Vistoria e Estado do Carro
Um carro usado só vale o que está em bom estado. Um inspetor dá nota ao carro, classifica cada peça e anexa um laudo. O anúncio só entra no ar se a vistoria for aprovada, então o comprador vê os mesmos fatos que o inspetor registrou.
A vistoria e seus itens
Uma vistoria tem uma nota geral de 0 a 5 e um laudo em PDF. Cada peça, como motor, freios ou pneus, tem sua própria linha com uma classificação de bom, regular, reparar ou trocar. As classificações explicam a nota, então o comprador vê onde estão os problemas.
Leituras de quilometragem
Guarde cada leitura de quilometragem do vendedor, do inspetor e do registro de manutenção. Se uma nova leitura for menor que uma anterior, o odômetro pode ter sido adulterado, então o sistema marca o anúncio para revisão em vez de mostrar um número errado.
Publicar só depois da vistoria
O anúncio passa para ativo somente quando existe uma vistoria com nota de pelo menos 3,0. A regra fica no banco de dados e também na aplicação, então nenhum caminho de código consegue publicar um carro não vistoriado.
Exemplo 31: Vistorias
Um vistoriador certificado dá nota a cada carro.
CREATE TABLE inspections (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
inspector_id BIGINT UNSIGNED NOT NULL,
overall_score DECIMAL(2,1) NOT NULL,
report_url VARCHAR(500) NULL,
inspected_at DATETIME NOT NULL,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (inspector_id) REFERENCES users(id),
CHECK (overall_score BETWEEN 0 AND 5)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 32: Uma vistoria de exemplo
Nota 4,5 de 5 com laudo em PDF.
INSERT INTO inspections (listing_id, inspector_id, overall_score, report_url, inspected_at)
SELECT 1, u.id, 4.5, 'https://cdn.example.com/reports/1.pdf', NOW()
FROM users u WHERE u.email = '[email protected]';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 33: Itens da vistoria
Classifique cada peça: boa, regular, reparar ou trocar.
CREATE TABLE inspection_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
inspection_id BIGINT UNSIGNED NOT NULL,
part VARCHAR(60) NOT NULL,
grade ENUM('good','fair','repair','replace') NOT NULL,
note VARCHAR(255) NULL,
FOREIGN KEY (inspection_id) REFERENCES inspections(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 34: Itens de vistoria de exemplo
Pastilhas de freio com 40 por cento e um arranhão na porta.
INSERT INTO inspection_items (inspection_id, part, grade, note) VALUES
(1, 'engine', 'good', 'No leaks'),
(1, 'brakes', 'fair', 'Front pads at 40 percent'),
(1, 'tyres', 'good', 'Above 4 mm tread'),
(1, 'paint', 'repair', 'Small scratch on the rear door');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 35: Encontrar as peças que precisam de reparo
Conte os problemas por anúncio.
SELECT i.listing_id, COUNT(*) AS problems
FROM inspection_items ii
JOIN inspections i ON i.id = ii.inspection_id
WHERE ii.grade IN ('repair','replace')
GROUP BY i.listing_id;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 36: Publicar somente após a vistoria
O anúncio só vai ao ar com nota aprovada.
UPDATE car_listings l
SET l.status = 'active'
WHERE l.id = 2
AND l.status = 'pending_review'
AND EXISTS (SELECT 1 FROM inspections i WHERE i.listing_id = l.id AND i.overall_score >= 3.0);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 37: Maquete: o laudo da vistoria
As classificações exibidas na página do anúncio.
<section class="inspection">
<h2>Inspection report</h2>
<p>Overall score <strong>4.5 / 5</strong> · Inspected 12 Oct 2026</p>
<ul class="grades">
<li class="good">Engine: good</li>
<li class="fair">Brakes: fair, front pads at 40%</li>
<li class="good">Tyres: good</li>
<li class="repair">Paint: small scratch on the rear door</li>
</ul>
<a href="https://cdn.example.com/reports/1.pdf">Download the full PDF</a>
</section>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 38: Leituras de quilometragem
Guarde cada leitura para detectar odômetro adulterado.
CREATE TABLE mileage_readings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
reading_km INT UNSIGNED NOT NULL,
source ENUM('seller','inspector','service_record') NOT NULL,
read_at DATETIME NOT NULL,
FOREIGN KEY (listing_id) REFERENCES car_listings(id) ON DELETE CASCADE,
INDEX idx_mileage_listing_time (listing_id, read_at)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 6: Busca, Filtros e Desempenho
Compradores pesquisam por província, preço, ano, combustível e câmbio, e esperam resultados em bem menos de um segundo. Índices nas colunas que aparecem em WHERE e ORDER BY tornam isso possível, e a paginação por chave mantém as páginas profundas rápidas.
Índices que combinam com a consulta
Um índice composto em status, província, preço e ano atende a busca comum. A ordem das colunas importa: coloque as colunas de igualdade primeiro e a coluna de intervalo por último. O EXPLAIN mostra se o índice é usado, e o EXPLAIN ANALYZE mostra o tempo real.
Busca textual e filtros com junção
Um índice FULLTEXT na descrição permite procurar palavras como teto solar ou couro. Combustível e câmbio ficam na tabela de versões, então a busca faz junção com essa tabela, e a junção usa o índice da versão.
Paginação por chave e buscas salvas
O OFFSET lê e descarta todas as linhas anteriores, então páginas mais profundas ficam mais lentas. A paginação por chave lembra a última data de criação e o último id e continua a partir deles. Uma busca salva guarda os filtros em JSON para que o comprador receba os novos anúncios que combinam.
Exemplo 39: Um índice de busca
Indexe status, província, preço e ano juntos.
CREATE INDEX idx_listings_search
ON car_listings (status, province, asking_price, year);
EXPLAIN SELECT id, asking_price FROM car_listings
WHERE status = 'active' AND province = 'Chonburi' AND asking_price BETWEEN 500000 AND 900000;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 40: Busca em texto completo
Pesquise a descrição com MATCH AGAINST.
ALTER TABLE car_listings ADD COLUMN description TEXT NULL,
ADD FULLTEXT INDEX ft_listings_description (description);
SELECT id, asking_price FROM car_listings
WHERE status = 'active' AND MATCH(description) AGAINST ('sunroof leather' IN NATURAL LANGUAGE MODE);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 41: Filtrar por combustível e câmbio
Junte a tabela de versões para achar carros híbridos CVT.
SELECT l.id, l.asking_price, t.fuel, t.transmission
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
WHERE l.status = 'active' AND t.fuel = 'hybrid' AND t.transmission = 'cvt'
ORDER BY l.asking_price
LIMIT 20;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 42: Paginação por chave (keyset)
Percorra os resultados sem o OFFSET caro.
SELECT id, asking_price, created_at
FROM car_listings
WHERE status = 'active'
AND (created_at, id) < ('2026-10-01 10:00:00', 1000)
ORDER BY created_at DESC, id DESC
LIMIT 20;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 43: Buscas salvas
Guarde os filtros escolhidos pelo comprador em JSON.
CREATE TABLE saved_searches (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
filters JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
INSERT INTO saved_searches (user_id, filters)
SELECT id, JSON_OBJECT('make','Toyota','fuel','hybrid','max_price',900000)
FROM users WHERE email = '[email protected]';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 44: Maquete: a barra lateral de filtros
Filtros de marca, preço, ano e combustível com botão de exibir.
<aside class="filters">
<h3>Filters</h3>
<label>Make <select><option>Toyota</option><option>Honda</option></select></label>
<label>Price (THB) <input type="range" min="100000" max="2000000"></label>
<label>Year from <input type="number" value="2018"></label>
<fieldset>
<legend>Fuel</legend>
<label><input type="checkbox" checked> Hybrid</label>
<label><input type="checkbox"> Petrol</label>
</fieldset>
<button>Show 24 cars</button>
</aside>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 45: Contar carros por marca
Mostre os números ao lado de cada opção.
SELECT mk.name AS make, COUNT(*) AS cars
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
JOIN car_models m ON m.id = t.model_id
JOIN car_makes mk ON mk.id = m.make_id
WHERE l.status = 'active'
GROUP BY mk.name
ORDER BY cars DESC;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 46: Verificar o plano da consulta
Use EXPLAIN ANALYZE para ver se o índice é usado.
EXPLAIN ANALYZE
SELECT id FROM car_listings
WHERE status = 'active' AND province = 'Chonburi'
ORDER BY asking_price
LIMIT 20;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 7: Ofertas e Negociação
Uma oferta é a proposta de um comprador por um carro, com prazo de validade. O preço pedido permanece o mesmo, então vários compradores podem fazer ofertas, e o vendedor pode aceitar uma, recusar as outras ou fazer uma contraproposta com um novo preço.
Ofertas em tabela própria
Uma oferta é uma linha com o anúncio, o comprador, o valor, o status e a data de expiração. A contraproposta do vendedor é uma linha nova que aponta para a oferta do comprador, então toda a negociação continua visível no histórico.
Preço mínimo e verificações
O vendedor pode definir um preço mínimo. Compare a oferta com o mínimo antes de mostrá-la ao vendedor como aceitável. Guarde a decisão com o motivo, para que disputas possam ser respondidas depois.
Expiração e a maquete
Um trabalho agendado marca as ofertas antigas como expiradas. O diálogo de fazer oferta mostra a data de expiração e explica o que o vendedor pode fazer, para que o comprador saiba o que acontece em seguida.
Exemplo 47: Ofertas
O comprador propõe um preço; o preço anunciado não muda.
CREATE TABLE offers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
buyer_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status ENUM('open','accepted','rejected','withdrawn','expired') NOT NULL DEFAULT 'open',
expires_at DATETIME NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (buyer_id) REFERENCES users(id),
INDEX idx_offers_listing_status (listing_id, status),
CHECK (amount > 0)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 48: Uma oferta de exemplo
O comprador oferece 720.000 baht por 48 horas.
INSERT INTO offers (listing_id, buyer_id, amount, expires_at)
SELECT 1, u.id, 720000.00, DATE_ADD(NOW(), INTERVAL 48 HOUR)
FROM users u WHERE u.email = '[email protected]';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 49: Maquete: o diálogo de oferta
Campo de valor, aviso de validade e botão de envio.
<dialog id="offer">
<form method="dialog">
<h2>Make an offer</h2>
<p>Asking price: ฿765,000</p>
<label>Your offer (THB)
<input type="number" min="1" step="1000" value="720000" required>
</label>
<p class="hint">The offer expires in 48 hours. The seller can accept, reject or counter.</p>
<button value="cancel">Cancel</button>
<button value="send" class="primary">Send offer</button>
</form>
</dialog>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 50: Recusar uma oferta baixa
Compare a oferta com o mínimo que o vendedor aceita.
ALTER TABLE car_listings ADD COLUMN min_offer DECIMAL(12,2) NULL;
UPDATE car_listings SET min_offer = 700000.00 WHERE id = 1;
SELECT CASE WHEN 650000.00 < min_offer THEN 'reject: below minimum' ELSE 'accept for review' END AS decision
FROM car_listings WHERE id = 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 51: Aceitar uma oferta em uma transação
Trave o carro, aceite a oferta e reserve o anúncio juntos.
START TRANSACTION;
SELECT id, status FROM car_listings WHERE id = 1 FOR UPDATE;
UPDATE offers SET status = 'accepted' WHERE id = 1 AND status = 'open';
UPDATE car_listings SET status = 'reserved' WHERE id = 1 AND status = 'active';
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 52: Recusar as outras ofertas
Quando uma oferta vence, as outras do mesmo carro são encerradas.
UPDATE offers
SET status = 'rejected'
WHERE listing_id = 1 AND status = 'open' AND id <> 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 53: Expirar ofertas antigas
Um trabalho agendado encerra as ofertas que passaram do prazo.
UPDATE offers
SET status = 'expired'
WHERE status = 'open' AND expires_at < NOW();
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 54: Uma contraproposta do vendedor
Guarde a contraproposta como nova oferta ligada à do comprador.
ALTER TABLE offers ADD COLUMN parent_offer_id BIGINT UNSIGNED NULL,
ADD COLUMN made_by ENUM('buyer','seller') NOT NULL DEFAULT 'buyer';
INSERT INTO offers (listing_id, buyer_id, amount, expires_at, parent_offer_id, made_by)
SELECT 1, buyer_id, 735000.00, DATE_ADD(NOW(), INTERVAL 24 HOUR), id, 'seller'
FROM offers WHERE id = 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 8: Pedidos e o Fluxo de Status
Quando um vendedor aceita uma oferta, um pedido é criado. O pedido passa por um conjunto fixo de status, e cada mudança é registrada, para que a equipe de suporte sempre veja como uma venda chegou ao estado atual.
Um pedido confirmado por carro
A coluna listing_id da tabela orders tem uma restrição UNIQUE, então o banco recusa um segundo pedido para o mesmo carro, mesmo que dois pedidos cheguem ao mesmo tempo. O sinal devido é calculado a partir do preço final, aqui 10 por cento.
Criar o pedido na mesma transação
Aceitar a oferta, reservar o anúncio e inserir o pedido acontecem dentro de uma única transação. Se qualquer etapa falhar, todas são revertidas, então um carro nunca fica reservado sem pedido, nem existe pedido sem carro reservado.
Histórico de status e etapas permitidas
Cada mudança no pedido é gravada em uma tabela de histórico com o status antigo, o novo status, quem fez e a hora. O UPDATE verifica o status atual, então um pedido não pode pular de aguardando sinal direto para concluído.
Exemplo 55: Uma tabela de pedidos
Uma compra confirmada por carro, com preço final e sinal.
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL UNIQUE,
buyer_id BIGINT UNSIGNED NOT NULL,
seller_id BIGINT UNSIGNED NOT NULL,
final_price DECIMAL(12,2) NOT NULL,
deposit_due DECIMAL(12,2) NOT NULL,
status ENUM('pending_deposit','deposit_paid','documents_pending','completed','cancelled','refunded') NOT NULL DEFAULT 'pending_deposit',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (buyer_id) REFERENCES users(id),
FOREIGN KEY (seller_id) REFERENCES users(id),
CHECK (final_price > 0),
CHECK (deposit_due <= final_price)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 56: Criar o pedido a partir da oferta aceita
Insira o pedido na mesma transação da aceitação.
START TRANSACTION;
SELECT status FROM car_listings WHERE id = 1 FOR UPDATE;
INSERT INTO orders (listing_id, buyer_id, seller_id, final_price, deposit_due)
SELECT o.listing_id, o.buyer_id, l.seller_id, o.amount, ROUND(o.amount * 0.10, 2)
FROM offers o JOIN car_listings l ON l.id = o.listing_id
WHERE o.id = 1 AND o.status = 'accepted';
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 57: Histórico de status do pedido
Registre cada mudança de status, com quem a fez e quando.
CREATE TABLE order_status_history (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
from_status VARCHAR(30) NULL,
to_status VARCHAR(30) NOT NULL,
actor_id BIGINT UNSIGNED NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
INDEX idx_history_order (order_id, changed_at)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 58: Avançar o pedido uma etapa
Mude o status somente a partir da etapa esperada.
UPDATE orders
SET status = 'deposit_paid'
WHERE id = 1 AND status = 'pending_deposit';
INSERT INTO order_status_history (order_id, from_status, to_status, actor_id)
VALUES (1, 'pending_deposit', 'deposit_paid', 2);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 59: Cancelar um pedido e liberar o carro
Devolva o anúncio para ativo quando o pedido for cancelado cedo.
START TRANSACTION;
UPDATE orders SET status = 'cancelled' WHERE id = 1 AND status IN ('pending_deposit','deposit_paid');
UPDATE car_listings SET status = 'active' WHERE id = 1 AND status = 'reserved';
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 60: Maquete: a página de andamento do pedido
Etapas desde a oferta aceita até a venda concluída.
<section class="order">
<h2>Order #1 · Toyota Camry G 2.5 (2019)</h2>
<ol class="steps">
<li class="done">Offer accepted</li>
<li class="done">Deposit paid ฿76,500</li>
<li class="current">Documents in progress</li>
<li>Ownership transferred</li>
<li>Completed</li>
</ol>
<button>Cancel order</button>
<p class="hint">You can cancel until the documents step starts.</p>
</section>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 9: Pagamentos como Livro-Razão
O dinheiro nunca é guardado como um único número que vai sendo editado. Cada pagamento, estorno e saldo é uma linha em um livro-razão. Os totais são calculados a partir das linhas, então o histórico fica sempre completo e os números sempre batem.
Uma linha por movimentação de dinheiro
Um pagamento tem um tipo, como sinal, saldo ou estorno, um valor, uma referência do provedor e um status. A referência do provedor é UNIQUE, então o mesmo pagamento não pode ser registrado duas vezes, e a linha se liga ao registro do banco ou do próprio provedor.
Webhooks que chegam duas vezes
Provedores de pagamento podem enviar a mesma notificação mais de uma vez. A atualização só muda o status quando a linha ainda está pendente, então a segunda mensagem não altera nada. Isso torna o tratador seguro para novas tentativas.
Estornos e o valor líquido pago
Um estorno é uma linha nova, não um pagamento negativo que sobrescreve o antigo. O valor líquido pago de um pedido é a soma dos pagamentos bem-sucedidos menos os estornos bem-sucedidos. Uma verificação antes da publicação interrompe a liberação se o valor líquido pago passar do preço final.
Exemplo 61: Uma tabela de pagamentos
Cada movimentação de dinheiro é uma linha com referência do provedor.
CREATE TABLE payments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
kind ENUM('deposit','balance','refund') NOT NULL,
amount DECIMAL(12,2) NOT NULL,
provider VARCHAR(40) NOT NULL,
provider_ref VARCHAR(100) NOT NULL UNIQUE,
status ENUM('pending','succeeded','failed') NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (amount > 0)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 62: Registrar o sinal antes da chamada de pagamento
Guarde o pagamento como pendente primeiro e depois atualize.
INSERT INTO payments (order_id, kind, amount, provider, provider_ref, status)
VALUES (1, 'deposit', 76500.00, 'example-pay', 'ref-0001', 'pending');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 63: Tratar um webhook de pagamento repetido
Atualize apenas uma vez, para que a mensagem duplicada não mude nada.
UPDATE payments
SET status = 'succeeded'
WHERE provider_ref = 'ref-0001' AND status = 'pending';
-- 0 rows changed on the second call, so the order is not moved twice
UPDATE orders SET status = 'deposit_paid'
WHERE id = 1 AND status = 'pending_deposit'
AND (SELECT status FROM payments WHERE provider_ref = 'ref-0001') = 'succeeded';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 64: Total líquido pago por pedido
Some sinal e saldo e subtraia reembolsos, a partir do livro-razão.
SELECT o.id,
o.final_price,
COALESCE(SUM(CASE WHEN p.kind IN ('deposit','balance') AND p.status = 'succeeded' THEN p.amount END), 0)
- COALESCE(SUM(CASE WHEN p.kind = 'refund' AND p.status = 'succeeded' THEN p.amount END), 0) AS net_paid
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
WHERE o.id = 1
GROUP BY o.id, o.final_price;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 65: Reembolsar o sinal
Insira uma linha de reembolso ligada ao sinal original.
INSERT INTO payments (order_id, kind, amount, provider, provider_ref, status)
SELECT order_id, 'refund', amount, provider, CONCAT('refund-', provider_ref), 'pending'
FROM payments
WHERE order_id = 1 AND kind = 'deposit' AND status = 'succeeded';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 66: Maquete: a tela de pagamento do sinal
Valor do sinal, aviso de retenção e botão de pagar.
<form class="payment">
<h2>Pay the deposit</h2>
<p>Deposit (10%): <strong>฿76,500</strong></p>
<p class="hint">Held until the ownership transfer is done. Refunded if the sale fails.</p>
<label>Card number <input name="card" autocomplete="cc-number" placeholder="•••• •••• •••• ••••"></label>
<button type="submit" class="primary">Pay ฿76,500</button>
</form>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 10: Transferência de Propriedade e Documentos
Em muitos países, um carro só troca de dono no órgão de trânsito, com papéis assinados. O banco de dados modela isso como uma lista de etapas, cada uma com seus documentos, para que o comprador veja exatamente o que falta e a equipe verifique cada documento antes de o carro ser marcado como vendido.
Etapas como linhas
Cada etapa da transferência tem um número, um nome e um status: a fazer, em andamento, concluída ou bloqueada. O pedido só é concluído quando todas as etapas estiverem prontas, o que é verificado na mesma transação que marca o carro como vendido.
Documentos e verificação
Cada documento tem um tipo, como o contrato de compra e venda ou o certificado de registro, uma URL do arquivo, quem enviou e a data em que foi verificado. Uma etapa só pode ser marcada como concluída quando o documento dela foi verificado, então uma foto não verificada de um formulário não basta.
A lista de verificação para o comprador
A maquete mostra ao comprador uma lista com marcações e um botão de envio. A lista é gerada a partir da tabela de etapas, então a tela e o banco de dados sempre concordam sobre o que já foi concluído.
Exemplo 67: Etapas da transferência
Uma linha por etapa da mudança de propriedade.
CREATE TABLE transfer_steps (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
step_no TINYINT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
status ENUM('todo','doing','done','blocked') NOT NULL DEFAULT 'todo',
done_at DATETIME NULL,
UNIQUE KEY uq_step (order_id, step_no),
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 68: Etapas padrão de um pedido
Cinco etapas, do contrato de venda à entrega das chaves.
INSERT INTO transfer_steps (order_id, step_no, name) VALUES
(1, 1, 'Seller signs the sale agreement'),
(1, 2, 'Buyer checks the registration book'),
(1, 3, 'Land Transport Office appointment booked'),
(1, 4, 'Ownership changed at the office'),
(1, 5, 'Balance paid and keys handed over');
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 69: Documentos
Guarde cada documento enviado com seu tipo e a data da verificação.
CREATE TABLE documents (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
doc_type ENUM('sale_agreement','registration_book','id_card','tax_receipt','transfer_form') NOT NULL,
file_url VARCHAR(500) NOT NULL,
uploaded_by BIGINT UNSIGNED NOT NULL,
verified_at DATETIME NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users(id)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 70: Concluir a etapa só com documento verificado
Confira o documento antes de marcar a etapa como feita.
UPDATE transfer_steps s
SET s.status = 'done', s.done_at = NOW()
WHERE s.order_id = 1 AND s.step_no = 1
AND EXISTS (SELECT 1 FROM documents d
WHERE d.order_id = s.order_id AND d.doc_type = 'sale_agreement' AND d.verified_at IS NOT NULL);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 71: Maquete: a lista de verificação da transferência
Caixas de seleção para cada etapa com botão de envio.
<ul class="checklist">
<li><input type="checkbox" checked disabled> Seller signed the sale agreement</li>
<li><input type="checkbox" checked disabled> Registration book checked</li>
<li><input type="checkbox"> Book an appointment at the office</li>
<li><input type="checkbox"> Ownership changed</li>
<li><input type="checkbox"> Balance paid</li>
</ul>
<button>Upload a document</button>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 72: Mudar a propriedade no fim
Conclua o pedido somente quando todas as etapas estiverem prontas.
START TRANSACTION;
UPDATE transfer_steps SET status = 'done', done_at = NOW()
WHERE order_id = 1 AND step_no = 4 AND status = 'doing';
UPDATE orders SET status = 'completed'
WHERE id = 1 AND status = 'documents_pending'
AND NOT EXISTS (SELECT 1 FROM transfer_steps WHERE order_id = 1 AND status <> 'done');
UPDATE car_listings SET status = 'sold' WHERE id = 1 AND status = 'reserved';
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 11: Financiamento e Garantias
A maioria dos compradores não paga o valor total à vista. Um financiamento bancário é uma solicitação ligada ao pedido, e uma garantia protege o comprador por um período de meses ou quilômetros após a venda.
Solicitações de financiamento
Uma solicitação de financiamento pertence a um pedido e registra o banco, o valor, o prazo e o status: enviada, aprovada, recusada ou retirada. O prazo é limitado por uma regra CHECK, como 6 a 84 meses, então empréstimos impossíveis não podem ser cadastrados.
Garantias a partir da data da venda
A garantia só é criada quando o pedido é concluído. Ela guarda a quantidade de meses, o limite de quilômetros e a data de início. Verificar se ela ainda vale compara a data e a quilometragem ao mesmo tempo, então o comprador recebe a resposta correta.
Mostrar a oferta com clareza
A página do pedido mostra a parcela mensal como estimativa e as condições da garantia em linguagem simples. Informações claras reduzem disputas depois da venda, o que é mais barato do que tratar reclamações mais tarde.
Exemplo 73: Pedidos de financiamento
O comprador pede um empréstimo ao banco para a compra.
CREATE TABLE financing_applications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
bank VARCHAR(80) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
term_months SMALLINT UNSIGNED NOT NULL,
status ENUM('submitted','approved','declined','withdrawn') NOT NULL DEFAULT 'submitted',
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (term_months BETWEEN 6 AND 84)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 74: Um pedido de exemplo
Pedido de empréstimo de 60 meses de 600.000 baht.
INSERT INTO financing_applications (order_id, bank, amount, term_months)
VALUES (1, 'Example Bank', 600000.00, 60);
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 75: Garantias
Uma garantia por pedido, com meses e quilômetros.
CREATE TABLE warranties (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL UNIQUE,
months SMALLINT UNSIGNED NOT NULL,
km_limit INT UNSIGNED NOT NULL,
starts_on DATE NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 76: Iniciar a garantia quando a venda for concluída
Crie a garantia a partir do pedido concluído.
INSERT INTO warranties (order_id, months, km_limit, starts_on)
SELECT id, 6, 10000, CURDATE() FROM orders WHERE id = 1 AND status = 'completed';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 77: Verificar se a garantia ainda vale
Compare a data e a quilometragem com os limites.
SELECT w.id,
(CURDATE() < DATE_ADD(w.starts_on, INTERVAL w.months MONTH)) AND (48250 + 1500 <= 48250 + w.km_limit) AS still_valid
FROM warranties w WHERE w.order_id = 1;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 78: Maquete: a caixa de empréstimo e garantia
Parcela estimada e condições da garantia na página do pedido.
<aside class="box">
<h3>Pay with a loan</h3>
<p>Example Bank · 60 months</p>
<p>Monthly payment is an estimate: <strong>฿11,800</strong></p>
<p class="status">Status: Submitted</p>
<h3>Warranty</h3>
<p>6 months or 10,000 km, whichever comes first</p>
</aside>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 12: Avaliações, Denúncias e Confiança
Um marketplace só funciona se as pessoas confiarem nele. As avaliações chegam depois de uma venda concluída, as denúncias permitem que os usuários sinalizem problemas, e as regras que escondem anúncios precisam ser claras e justas, para que um único usuário irritado não consiga esconder um bom carro.
Avaliações depois da venda
Uma avaliação só é permitida para um pedido concluído, e cada pessoa pode avaliar uma vez por pedido. As notas ficam limitadas a 1 a 5 por uma regra CHECK. A média só aparece depois de três avaliações, para que uma única avaliação não faça um vendedor parecer perfeito ou péssimo.
Denúncias de várias pessoas
Uma denúncia diz o que está errado, como um preço falso ou uma quilometragem incorreta. Conte os denunciantes distintos antes de esconder um anúncio, para que uma pessoa sozinha não consiga esconder um carro. Anúncios escondidos vão para uma fila de análise da equipe.
Maquete das ações de avaliação e denúncia
O formulário de avaliação pede estrelas e um comentário, e o link de denúncia fica visível mas sem atrapalhar. Um bom design torna a denúncia fácil e a publicação acidental de uma avaliação difícil.
Exemplo 79: Avaliações após a venda concluída
Uma avaliação por pessoa por pedido, nota de 1 a 5.
CREATE TABLE reviews (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
reviewer_id BIGINT UNSIGNED NOT NULL,
reviewee_id BIGINT UNSIGNED NOT NULL,
rating TINYINT UNSIGNED NOT NULL,
comment VARCHAR(1000) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_review (order_id, reviewer_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (rating BETWEEN 1 AND 5)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 80: Adicionar avaliação a um pedido concluído
Somente pedidos concluídos podem receber avaliação.
INSERT INTO reviews (order_id, reviewer_id, reviewee_id, rating, comment)
SELECT o.id, o.buyer_id, o.seller_id, 5, 'Car as described, smooth transfer.'
FROM orders o WHERE o.id = 1 AND o.status = 'completed';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 81: Nota média de cada vendedor
Mostre a nota do vendedor somente depois de três avaliações.
SELECT r.reviewee_id, ROUND(AVG(r.rating), 2) AS avg_rating, COUNT(*) AS reviews
FROM reviews r
GROUP BY r.reviewee_id
HAVING COUNT(*) >= 3;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 82: Denúncias de anúncios
Compradores e vendedores podem denunciar um anúncio suspeito.
CREATE TABLE listing_reports (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
reporter_id BIGINT UNSIGNED NOT NULL,
reason ENUM('fake_price','stolen','wrong_mileage','duplicate','other') NOT NULL,
status ENUM('open','investigating','closed') NOT NULL DEFAULT 'open',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (reporter_id) REFERENCES users(id)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 83: Ocultar um anúncio após várias denúncias
Conte os denunciantes distintos antes de agir.
SELECT listing_id, COUNT(DISTINCT reporter_id) AS reporters
FROM listing_reports
WHERE status = 'open'
GROUP BY listing_id
HAVING COUNT(DISTINCT reporter_id) >= 3;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 84: Maquete: o formulário de avaliação e o botão de denúncia
Nota em estrelas, caixa de comentário e link de denúncia.
<form class="review">
<h3>Rate your purchase</h3>
<div class="stars" role="radiogroup" aria-label="Rating">
<label><input type="radio" name="rating" value="1"> 1</label>
<label><input type="radio" name="rating" value="5" checked> 5</label>
</div>
<textarea maxlength="1000" placeholder="How was the car and the transfer?"></textarea>
<button>Post review</button>
<button type="button" class="link">Report this listing</button>
</form>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 13: Transações na Prática: Condições de Corrida
Dois compradores podem apertar Aceitar no mesmo instante. Sem cuidado, as duas requisições leem que o carro ainda está ativo e ambas o vendem. Este tema mostra como transações, travas de linha, a ordem das travas e chaves de idempotência evitam esse problema e outras falhas de um sistema real.
O que acontece sem trava
A sessão A lê o carro como ativo. A sessão B lê o mesmo status. As duas atualizam para reservado e as duas criam um pedido. O carro é vendido duas vezes. Uma trava na linha do carro, obtida com SELECT FOR UPDATE, faz a segunda sessão esperar até a primeira confirmar, e então a segunda vê reservado e para.
Verificar o número de linhas alteradas
Um UPDATE com WHERE status = active altera uma linha para o vencedor e zero linhas para o perdedor. A aplicação lê a contagem de linhas afetadas e devolve 409 Conflict ao perdedor, então o comprador vê uma mensagem clara em vez de um falso sucesso.
Deadlocks, reversões e novas tentativas
Duas transações que travam as mesmas linhas em ordens diferentes podem sofrer deadlock, e o MySQL reverte uma delas. Trave o carro primeiro, depois as ofertas e depois os pedidos, em todos os caminhos de código. Quando algo falha no meio, o ROLLBACK desfaz todas as mudanças dessa transação, e quem chamou pode tentar de novo com segurança usando a mesma chave de idempotência.
Exemplo 85: Dois compradores aceitam ao mesmo tempo
Trave a linha para que apenas uma transação vença.
-- Session A
START TRANSACTION;
SELECT status FROM car_listings WHERE id = 1 FOR UPDATE;
-- Session B waits here until A commits, then reads 'reserved' and stops.
UPDATE car_listings SET status = 'reserved' WHERE id = 1 AND status = 'active';
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 86: Verificar o número de linhas alteradas
A contagem 1 significa que esta requisição venceu; 0 significa que perdeu.
UPDATE car_listings SET status = 'reserved'
WHERE id = 1 AND status = 'active';
-- Application code: if ROW_COUNT() = 0, return 409 Conflict to the second buyer.
SELECT ROW_COUNT() AS won;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 87: Travar em uma ordem fixa
Todo caminho de código trava o carro antes das ofertas para evitar deadlocks.
-- Every code path locks in this order: car_listings, then offers, then orders
START TRANSACTION;
SELECT id FROM car_listings WHERE id = 1 FOR UPDATE;
SELECT id FROM offers WHERE listing_id = 1 FOR UPDATE;
COMMIT;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 88: Reverter quando o carro for tomado
Desfaça o pedido se a reserva falhar no meio do caminho.
START TRANSACTION;
UPDATE car_listings SET status = 'reserved' WHERE id = 2 AND status = 'active';
-- If the car was not reserved, nothing else should happen.
-- Application code: when ROW_COUNT() = 0, run ROLLBACK instead of inserting the order.
ROLLBACK;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 89: Chaves de idempotência
Uma requisição repetida com a mesma chave devolve o primeiro resultado.
CREATE TABLE idempotency_keys (
key_value CHAR(36) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
response JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
INSERT IGNORE INTO idempotency_keys (key_value, user_id)
VALUES ('5f3c2a10-0000-4000-8000-000000000001', 2);
-- ROW_COUNT() = 0 means the key already exists: return the stored response.
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 90: Um log de auditoria
Guarde quem alterou o quê, com os valores antigos e novos.
CREATE TABLE audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
actor_id BIGINT UNSIGNED NULL,
action VARCHAR(60) NOT NULL,
entity VARCHAR(40) NOT NULL,
entity_id BIGINT UNSIGNED NOT NULL,
detail JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_entity (entity, entity_id, created_at)
) ENGINE=InnoDB;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 14: Relatórios e Análises
O negócio precisa de números: vendas por dia, valor de estoque por revenda e o funil de anúncio até a venda. Os relatórios leem as mesmas tabelas, então precisam usar os status corretos e nunca contar um pedido cancelado como receita.
Vendas diárias a partir de pedidos concluídos
Conte apenas pedidos concluídos como receita. Agrupe pelo dia em que o pedido foi criado, ou pelo dia em que foi concluído, se a contabilidade precisar disso. Explique a escolha no relatório, para que duas pessoas que leem o mesmo número concordem sobre o que ele significa.
Visões para revendas
Uma view entrega à revenda a quantidade de estoque, o valor do estoque e a média de dias anunciados em um só lugar. A view esconde as junções, então a aplicação consulta a view como uma tabela, e as regras ficam em uma única definição.
O funil de conversão e o painel
Conte os carros anunciados, os carros com pelo menos uma oferta e os pedidos concluídos. O painel mostra os números principais no topo e uma tabela de carros ativos embaixo, para que o vendedor veja primeiro o que precisa de atenção.
Exemplo 91: Um relatório diário de vendas
Valor bruto dos pedidos concluídos por dia.
SELECT DATE(o.created_at) AS day,
COUNT(*) AS orders,
SUM(o.final_price) AS gross_value
FROM orders o
WHERE o.status = 'completed'
GROUP BY DATE(o.created_at)
ORDER BY day DESC;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 92: Uma visão do estoque da revenda
Carros, valor do estoque e dias anunciados para cada revenda.
CREATE VIEW v_dealer_stock AS
SELECT d.id AS dealer_id,
COUNT(l.id) AS cars,
SUM(l.asking_price) AS stock_value,
AVG(DATEDIFF(NOW(), l.created_at)) AS avg_days_listed
FROM dealers d
JOIN car_listings l ON l.seller_id = d.user_id
WHERE l.status = 'active'
GROUP BY d.id;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 93: Um funil de conversão
Conte carros anunciados, carros com ofertas e pedidos concluídos.
SELECT
(SELECT COUNT(*) FROM car_listings WHERE status IN ('active','reserved','sold')) AS listed,
(SELECT COUNT(DISTINCT listing_id) FROM offers) AS got_offers,
(SELECT COUNT(*) FROM orders WHERE status = 'completed') AS completed;
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 94: Maquete: o painel do vendedor
Números principais e uma tabela de carros ativos.
<section class="dashboard">
<div class="kpi"><span>Active cars</span><strong>12</strong></div>
<div class="kpi"><span>Open offers</span><strong>3</strong></div>
<div class="kpi"><span>Sold this month</span><strong>2</strong></div>
<table>
<tr><th>Car</th><th>Price</th><th>Offers</th><th>Days listed</th></tr>
<tr><td>Camry 2019</td><td>฿765,000</td><td>2</td><td>14</td></tr>
</table>
</section>
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Tópico 15: Produção: API, Segurança, Backup e Lançamento
Um banco de dados que funciona no notebook ainda não é um marketplace. A produção precisa de um contrato de API limpo, usuários de banco com privilégio mínimo, backups que possam ser restaurados, mudanças de esquema seguras e uma lista de verificação de lançamento que impeça uma versão ruim de chegar aos compradores.
Contrato da API e idempotência
Devolva os anúncios em um formato JSON estável, com um cursor para a próxima página. Exija o cabeçalho Idempotency-Key para ofertas e pedidos, para que uma nova tentativa após um tempo limite não crie um segundo pedido. Documente os valores de status, porque o app móvel e o app web precisam lê-los do mesmo jeito.
Privilégio mínimo, backup e migração
O usuário da aplicação pode ler, inserir e atualizar o banco de carros, mas não pode excluir pagamentos nem registros de auditoria. Faça o backup com um dump em transação única para não bloquear as gravações, e teste a restauração em outra máquina. Altere o esquema em pequenas etapas: adicione uma coluna que aceite NULL, preencha em lotes e só então aperte a regra em um lançamento posterior.
A lista de verificação de lançamento
Antes de cada lançamento, rode as verificações de negócio: nenhum carro ativo sem vistoria aprovada, e nenhum pedido em que o valor líquido pago seja maior que o preço final. Se alguma verificação falhar, o script de lançamento para, e uma regra quebrada nunca chega ao comprador.
Exemplo 95: A API pública de anúncios
O formato JSON da página de anúncios com cursor para a próxima página.
{
"method": "GET",
"path": "/api/v1/listings?province=Chonburi&max_price=900000&page_size=20",
"response": {
"items": [
{ "id": 1, "make": "Toyota", "model": "Camry", "trim": "G 2.5", "year": 2019, "mileage_km": 48250, "asking_price": 765000, "province": "Chonburi" }
],
"next_cursor": "eyJpZCI6MX0"
}
}
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 96: Fazer uma oferta pela API
Envie uma chave de idempotência para que as tentativas sejam seguras.
curl -X POST https://api.example.com/api/v1/listings/1/offers \
-H "Authorization: Bearer <token>" \
-H "Idempotency-Key: 5f3c2a10-0000-4000-8000-000000000002" \
-H "Content-Type: application/json" \
-d '{"amount": 720000}'
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 97: Privilégio mínimo para a aplicação
O usuário da aplicação não pode excluir pagamentos nem registros de auditoria.
CREATE USER IF NOT EXISTS 'carmarket_app'@'%' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE ON carmarket.* TO 'carmarket_app'@'%';
-- No DELETE on orders, payments or audit_log: those are kept for legal reasons.
REVOKE DELETE ON carmarket.payments FROM 'carmarket_app'@'%';
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 98: Fazer backup sem bloquear gravações
Use um dump em transação única e teste a restauração.
mysqldump --single-transaction --routines --triggers carmarket \
| gzip > carmarket-$(date +%F).sql.gz
# Restore test on another machine:
gunzip -c carmarket-2026-10-30.sql.gz | mysql carmarket_restore_test
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 99: Uma mudança de schema segura
Adicione uma coluna que aceite NULL, preencha em lotes e só depois restrinja.
-- Step 1: add the column as nullable (old code keeps working)
ALTER TABLE car_listings ADD COLUMN accident_free TINYINT(1) NULL;
-- Step 2: backfill in batches, not in one huge UPDATE
UPDATE car_listings SET accident_free = 1 WHERE id BETWEEN 1 AND 1000 AND accident_free IS NULL;
-- Step 3 (later release): set NOT NULL after every row has a value.
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Exemplo 100: Uma lista de verificação de lançamento das regras de negócio
Interrompa o lançamento quando uma regra, como o total líquido pago, for quebrada.
#!/usr/bin/env bash
set -euo pipefail
# 1. No active listing without a passing inspection
mysql -N carmarket -e "SELECT COUNT(*) FROM car_listings l WHERE l.status='active' AND NOT EXISTS (SELECT 1 FROM inspections i WHERE i.listing_id=l.id AND i.overall_score>=3)" | grep -q '^0$'
# 2. Net paid never exceeds the final price
mysql -N carmarket -e "SELECT COUNT(*) FROM orders o JOIN (SELECT order_id, SUM(amount) s FROM payments WHERE status='succeeded' AND kind IN ('deposit','balance') GROUP BY order_id) p ON p.order_id=o.id WHERE p.s > o.final_price" | grep -q '^0$'
echo "business checks passed"
*Este exemplo não foi executado aqui, porque o MySQL não estava instalado no computador que preparou este artigo.*
Laboratório: Construir e Testar o Banco de Dados do Marketplace de Carros
Reúna o artigo em um banco de testes: crie as tabelas na ordem certa, insira carros e usuários de exemplo, leve uma oferta pela aceitação e pelo fluxo do pedido, simule dois compradores ao mesmo tempo e termine com a lista de verificação de lançamento.
1. Crie o banco carmarket com utf8mb4 e depois crie as tabelas users, dealers, car_makes, car_models e car_trims, nessa ordem.
2. Insira os usuários de exemplo, uma revenda, as marcas e modelos e uma versão, e depois confira a contagem de linhas com SELECT COUNT(*).
3. Crie car_listings, car_images, inspections e inspection_items, e insira um anúncio com fotos e uma vistoria aprovada.
4. Publique o anúncio somente pela regra que exige vistoria aprovada, e confirme que um anúncio sem vistoria continua aguardando análise.
5. Crie offers e orders, aceite uma oferta dentro de uma transação e confira o histórico de status do pedido.
6. Abra duas sessões do MySQL, trave o mesmo carro nas duas e mostre que apenas uma reserva tem sucesso.
7. Rode a lista de verificação de lançamento do exemplo 100 sobre os dados de teste e corrija qualquer regra quebrada antes de seguir em frente.
Próximos passos
Depois do laboratório, leia guias de modelagem de banco de dados para a sua pilha, acrescente as telas do app móvel a partir das maquetes e teste cada transação com duas sessões ao mesmo tempo antes de lançar. Guarde os scripts em um repositório e execute-os de novo após cada mudança de esquema, porque as regras do marketplace vão crescer junto com o negócio.
Resumo
Um marketplace de carros usados é um conjunto de regras que o banco de dados precisa manter: uma venda por carro, uma vistoria antes da publicação, um sinal retido até a transferência ser concluída e um livro-razão que sempre fecha. Você aprendeu a escrever as regras primeiro, a modelar usuários, revendas e o catálogo de carros, a publicar anúncios com fotos e vistorias, a buscar com índices e paginação por chave, a tratar ofertas e pedidos com fluxos de status claros, a registrar pagamentos como livro-razão, a acompanhar cada etapa da transferência de propriedade, a incluir financiamento e garantias, a construir confiança com avaliações e denúncias, a lidar com condições de corrida com travas e idempotência, a produzir relatórios de negócio e a lançar com segurança.