用 MySQL 单独搭建完整 CMS 不现实——它只是存储引擎,不处理路由、模板、用户登录或文件上传。真正能跑起来的“简单 CMS”,必须搭配 PHP/Python/Node.js 等后端语言,MySQL 仅负责存取文章、分类、用户等结构化数据。
建表前先理清核心数据关系
一个最小可用 CMS 至少要支撑「用户登录」「发布文章」「打标签」「分页列表」。别一上来就设计 10 张表,先聚焦 4 张基础表:
users:存
id、
username、
password_hash(别存明文!)、
role(如 'admin' 或 'editor')
posts:含
id、
title、
content(TEXT 类型)、
author_id(关联 users.id)、
created_at、
status('draft'/'published')
categories:
id、
name、
slug
post_categories:纯关联表,字段只有
post_id和
category_id(支持一篇文章多个分类)
外键约束可加可不加,开发阶段建议先关掉
FOREIGN_KEY_CHECKS=0,避免 INSERT 顺序出错;上线前补上约束并验证数据一致性。
PHP + MySQL 实现文章列表的关键查询
前端要显示「已发布的文章标题+摘要+分类名+发布时间」,一条 JOIN 查询就能搞定,但要注意字段别名和 NULL 处理:
SELECT p.id, p.title, SUBSTRING(p.content, 1, 120) AS excerpt, c.name AS category_name, p.created_at FROM posts p LEFT JOIN post_categories pc ON p.id = pc.post_id LEFT JOIN categories c ON pc.category_id = c.id WHERE p.status = 'published' ORDER BY p.created_at DESC LIMIT 10;
常见坑:
SUBSTRING在 MySQL 8.0+ 支持,老版本用
SUBSTR;若 content 是 JSON 字段,不能直接截取,得先用
JSON_UNQUOTE(JSON_EXTRACT(...))LEFT JOIN 导致同一篇文章出现多行(多个分类),需在 PHP 层合并,或改用
GROUP_CONCAT(c.name)拼接分类名 没加
WHERE p.status = 'published'?测试数据会混进草稿,线上直接暴露未审核内容
密码存储和用户登录的硬性要求
MySQL 本身不校验密码强度或加密逻辑,这些必须由应用层完成:
注册时,PHP 必须用password_hash($password, PASSWORD_ARGON2ID)(优先)或
PASSWORD_DEFAULT生成哈希,存进
users.password_hash登录验证时,用
password_verify($input, $hash_from_db),绝不用
=或
MD5()MySQL 用户账号(如 root@localhost)和 CMS 应用用户(如 admin@example.com)完全无关,别把应用密码配到 MySQL 连接配置里传明文
如果跳过这步,哪怕界面再漂亮,系统也算不上“可用”——它只是个带 SQL 注入漏洞的玩具。
部署时最容易被忽略的 MySQL 配置项
本地能跑,放到服务器就报错?大概率是这几个参数没调:
sql_mode:默认可能含
STRICT_TRANS_TABLES,插入空字符串到 NOT NULL 字段会失败;CMS 表单常有可选字段,建议设为
""或至少去掉
STRICT相关项
max_allowed_packet:上传富文本(含图片 base64)时,
content字段超 4MB 就被截断,需调大到 32M+ 字符集:全程统一用
utf8mb4(不是 utf8!),否则 emoji 和生僻字存不进去,建库时就指定:
CREATE DATABASE cms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
这些不是“高级技巧”,而是让数据不丢、不乱、不报错的底线配置。
