CSDN-从27篇文档还原一套CMS开发规范

从 27 篇文档里"考古":一套老牌 PHP CMS 的完整开发规范是怎么还原出来的

本文记录一次真实的规范整理过程:拿到一个只有 27 篇帮助文档的国产 CMS(能鈺CMS),如何把它散落在各处的隐性规则,重建成一份可直接照着写代码的开发规范。方法可复用到任何"文档写得薄、代码写得厚"的老项目上。

写在前面

先说结论:这套 CMS 的官方文档只有 27 篇,插件开发部分正文不到 1000 字。 但你如果照着文档写插件,100% 会在第三个坑卡住。

因为真正的标准不在文档里,在官方示范插件的源码里。

我做的事情是:抓完 27 篇文档 → 下载官方示范插件包 → 解包逐行读 → 把「文档说什么」和「代码实际怎么跑」对齐 → 输出一份可执行的规范。

整理出的手册大约 40KB,下面挑最关键的部分展开。


一、先搞清楚:文档体系长什么样

板块 篇数 内容
安装说明 6 运行环境、目录结构、安装、支付配置
标签格式 11 内容页/列表页/单页/首页/公共/循环/判断/常量
模板设计 3 模板结构、模板说明、常见问题
二次开发 5 数据库字典、DB 操作、常用函数、安全注意
插件开发 2 插件说明、插件示范
支付设置 0 空栏目

第一个判断:标签格式占了 11/27。 这说明这套 CMS 的重心是模板层,不是后端框架层。也就是说,绝大部分二次开发需求,其实是用标签解决,而不是写 PHP。

第二个判断:插件开发 2 篇是这套文档最薄的地方。 薄到"插件结构"那一节直接用一张图片代替文字。

所以我专门去把示范插件包下载下来解包了。

小技巧:这类老 CMS 的文档站几乎都没有 sitemap,但文章 URL 通常是有规律的(这里是 /show/{id}.html)。直接 seq 1 27 循环抓,比翻菜单快得多。


二、模板设计规范:4 个文件决定生死

2.1 模板怎么被系统识别

模板放在 theme/ 下,一套一个文件夹。系统识别模板的依据只有一件事------这 4 个文件齐不齐:

复制代码
theme/你的模板/
├─ _config.php   模板指向配置文件
├─ _note.php     模板文件用途及注释(后台自动生成)
├─ _theme.php    模板配置信息:名称/作者/官网
└─ theme.jpg     模板缩略图,建议宽度 390px

名字一个都不能改,少一个模板就不显示,而且后台不会给你任何报错。

这是模板作者最容易"白忙一场"的地方------目录结构全对、CSS 写得漂亮,就因为漏了个 _note.php,后台界面管理里死活看不到这套模板。

2.2 编码与后缀

  • 必须 UTF-8:程序是 UTF-8,模板用 GBK 保存,前台直接乱码。
  • 后缀默认 .php,不是 .html。系统运行形态是「HTML + 标签」。

2.3 完整目录结构(这就是交付验收清单)

官方默认模板的目录,可以直接当 checklist 用:

复制代码
模板根/
├─ index.php        首页
├─ head.php / top.php / foot.php / center.php    公共头/顶/底/会员主页
├─ block/left.php                区块文件
├─ css/  images/  js/            静态资源
├─ mobile/                       手机站(响应式可省略)
├─ other/  label.php pay.php search.php sitemap.php tags.php
├─ page/   list.php page.php show.php comment.php
│          show_temp.php(下载类) show_video.php(视频类)
└─ user/   index.php login.php reg.php auth.php editpass.php ...
           bill/ money/ order/ other/ post/   会员各子模块

判断标准很简单 :做企业站,最少交付 index + list + page + show + search + head/top/foot;做知识付费站,user/ 必须全量补齐,还要加 show_video.php 和 show_temp.php。

2.4 UI 组件不是强制的

官方自己说了:不强制用自带 ui.css,可以用任意第三方 UI。唯一要求是所有功能点必须正常------登录、下单、分页、筛选、评论、投稿,一个都不能坏。

这句话翻译成人话:换 UI 框架可以,但换完要做功能回归。


三、标签语法:这套 CMS 真正的核心

3.1 标签体系总览

类别 形态 典型
变量 {$var} / {$rs[字段]} {$title}
常量 {WEB_ROOT}、{USER_ID} 12 个内置
系统变量 {cms[web_name]} 后台设置字段
数据循环 {cms:rs ...} 万能查库
数组循环 {loop data="$arr"} 遍历已有数组
计数循环 {for min max step var} 纯自增
判断 {if}{elseif}{else}{/if} 条件
包含/区块 {include} / {block("key")} 复用
转义 {no}...{/no} 保护 JS

3.2 最重要的两个坑

坑一:主表查不到自定义字段。

这套 CMS 把内容拆成了两张表:

cms_show(主表) cms_data(副表)
标题、缩略图、价格、销量、状态、排序 简介、正文、组图、视频、下载、网盘、SEO 自定义
php 复制代码
// ✅ 查副表,能调自定义字段
{cms:rs table="cms_data" where="isshow=1" cateid="1" order="ordnum desc,id desc"}
    {$i}. {$rs.title}
{/cms:rs}

// ❌ 查主表,{$rs.intro} 恒为空,还不报错
{cms:rs table="cms_show" cateid="1"}
    {$i}. {$rs.title} {$rs.intro}
{/cms:rs}

坑二:一个模板只能有一个 pagesize。

文档原话:"一个模板中,只能有一个 pagesize 参数。"两个列表都想分页 → 第二个列表分页必然错乱。没有报错,就是页码乱跳。

3.3 万能循环标签 {cms:rs} 参数表

这是整套标签里最需要背下来的一个,14 个参数:

参数 作用 必须 默认 备注
table 表名 是 --- 也可写 from
top 显示数量 否 10 0 = 全部
pagesize 分页数量 否 --- 全页唯一
field 查询字段 否 *
join 链表 否 ---
where 条件 否 1=1
order 排序 否 id desc
group 分组 否 ---
cateid 栏目 ID 否 --- 查内容时用
cache 是否缓存 否 false
cachetime 缓存时间 否 0 0 = 永久,单位分钟
num 分页数字间隔 否 3
key 分页自增字段 否 id 自增字段非 id 必须指定
auto 循环自增变量 否 i 多层循环必须改名

还自带一个 {rs:eof} 兜底标签,查不到数据时显示:

复制代码
{cms:rs table="cms_data" top="10"}
    {rs:eof}暂无内容{/rs:eof}
    {$rs.title}
{/cms:rs}

3.4 {no} 标签:救命的那个

模板引擎会把所有 {...} 当标签解析。你的 JS 里写 if(a){b()},会被引擎吃掉,然后:

页面不报错,样式正常,功能全废。

正确姿势:

复制代码
{no}
<script>
if (a) { b(); }
</script>
{/no}

这个坑我见过太多次了,排查起来极其痛苦,因为控制台一片干净。

3.5 常量表(12 个,建议背)

常量 值示例 用途
{WEB_URL} http://www.xx.com 站点域名
{WEB_ROOT} / 相对根路径
{THIS_LOCAL} /news 当前路径
{THIS_URL} http://.../show/1.html 当前 URL
{PRE_URL} / 上一页
{M_NAME} / {C_NAME} / {A_NAME} home / index / index 模块/控制器/方法
{WEB_THEME} /theme/default/ 模板路径
{IS_HOME} --- 是否首页
{USER_ID} 0 未登录为 0
{USER_GROUP} --- 会员组 ID

一条硬规范 :所有静态资源必须写成 {WEB_ROOT}public/... 或 {WEB_THEME}css/...,禁止 硬编码 /public/...。

原因:客户把站部署在子目录(www.xx.com/cms/)时,硬编码的绝对路径全部 404,整个站光秃秃的。

3.6 模板标签 10 条铁律

写模板前背下来,能省掉 80% 的调试时间:

  1. 资源前缀统一 {WEB_ROOT} / {WEB_THEME}
  2. 要自定义字段 → 查 cms_data
  3. 一页只允许一个 pagesize
  4. 嵌套循环换字母(rs / rs2 / rp),每层不同 auto
  5. 空数据必须给 {rs:eof}
  6. 含 {} 的 JS 全部包 {no}
  7. 文件必须 UTF-8
  8. 4 个 _ 文件 + theme.jpg 齐全
  9. 列表分页用 $pg->totalpage,内容分页用 $pagenum,别混
  10. 二级栏目判断 get_sonid_num(),否则空箭头

四、二次开发:能力边界在哪

4.1 官方明说的前置技能

MySQL 设计、PHP 语法、Div+Css、jQuery。文档原话是:

"如果你啥都不会,请节省大家时间,专业的事情交给专业的人处理。"

这句话挺实在的,建议直接写进给客户的报价前置说明里。

4.2 数据库 API(5 个方法)

php 复制代码
$this->db->row("SQL");                  // 读单条,返回一维数组
$this->db->load("SQL");                 // 读多条,返回二维数组
$this->db->add("表名", [数组]);          // 插入,主键取 $this->db->newid
$this->db->update("表名", "条件", [数组]); // 更新
$this->db->del("表名", "条件");          // 删除

注意 update / del 的第二个参数是直接拼进 SQL 的字符串,这是注入的高发点:

php 复制代码
// ✅ 强制转整型
$id = getint(F('id'));
$this->db->update("cms_show", "id=$id", $data);

// ❌ 直接拼接用户输入
$this->db->update("cms_show", "id=" . F('id'), $data);

4.3 常用函数(9 个,够用 90% 场景)

函数 作用
config('web_name') 读系统变量
T('home') 读模板指向路径
F('url') 取 GET/POST(已内置 XSS 过滤)
enhtml($v) 参数过滤
nohtml($v) 去 HTML 取纯文本
getint($v, 0) 强转整型,非法为 0
cut($v, 30, 1) 截字符串,第 3 参为 1 加省略号
S("key", $v) Session 存取删
C("key", $v) Cookie 存取删

F() 还支持强制指定来源:F('get.url') / F('post.url')。

扩展函数写在哪? 官方明确:写进 app/fun.php,前后台控制器都能直接调。这是唯一推荐的落点。

4.4 安全红线(照做,别问)

  1. 所有表单前后端双验证------前端只算体验,不算安全

  2. 所有 POST 必须带 Token :

    html 复制代码
    <input name="token" type="hidden" value="{$token}">
  3. 数据获取一律走 F() ,不要直接摸 $_GET / $_POST

  4. 上线前关掉 index.php 里的错误提示------错误页会暴露物理路径和 SQL

4.5 二开的三条铁律

铁律 原因
不改 class/、app/app.php、app/cms.php 升级即覆盖,改了就等着背锅
扩展函数进 app/fun.php 唯一官方认可落点
开发期开错误提示,上线关 安全

五、插件开发:文档最薄,坑最深

这一节是全文重点。因为官方只给了 2 篇文档、正文不到 1000 字,剩下全靠我读示范包源码推出来的。

5.1 目录规范(官方结构图 + 示范包双重确认)

复制代码
plug/
└─ test/                  插件文件夹名 = 插件唯一标识 root
   ├─ _config.php          【必须】配置,文件名不可改
   ├─ icon.jpg             【必须】图标
   ├─ app/                 程序代码
   │   ├─ index.php        前台控制器(非必须)
   │   ├─ admin.php        后台控制器(非必须)
   │   └─ hook.php         钩子类(hook=1 时必须有)
   └─ view/                插件模板
       └─ config.php       后台参数页

文件夹名只能英文、不能有特殊字符------因为它会变成类名和路由的一部分。

5.2 _config.php 全字段(12 个)

php 复制代码
<?php
return [
  'title'    => '百度推送',                    // 插件名称
  'icon'     => 'icon.jpg',                   // 图标,相对插件目录
  'intro'    => '百度URL批量推送API接口',        // 简介
  'author'   => '官方',                        // 作者
  'url'      => 'https://www.nengyu.com',     // 作者网址
  'version'  => '1.0',                        // 版本
  'install'  => 'DROP TABLE IF EXISTS cms_plug_push@@@@CREATE TABLE ...',  // 安装SQL
  'delete'   => 'DROP TABLE cms_plug_push',   // 卸载SQL
  'type'     => 'tool',                       // 类型
  'config'   => '{"url":""}',                 // 配置数据(JSON字符串!)
  'hook'     => '0',                          // 是否用 Hook
  'admin'    => '1',                          // 是否有后台
];

三个必踩的坑:

坑 1 --- config 必须是 JSON 字符串。 不是 PHP 数组。写成 ['url' => ''] 后台读不到配置,而且不报错。

坑 2 --- install / delete 多条 SQL 用 @@@@ 分隔,不是分号。 因为建表语句内部本身含分号,用分号会把 SQL 切坏。

坑 3 --- delete 必须幂等。 用户会重复点卸载,用 DROP TABLE IF EXISTS。

type 的枚举值:pay / sms / upload / auth / login / cash / safe / tool / other。

5.3 钩子机制

类名规则:plug_插件文件夹名 (示范里是 plug_test)。

php 复制代码
<?php
class plug_test
{
    private $cms;

    public function __construct($ref = null)
    {
        if ($ref) { $this->cms = $ref; }   // 系统注入核心对象

        // 注册:cms_hook::reg(钩子名, $this, 方法名)
        cms_hook::reg('route', $this, 'route');

        // 业务开关:关了就不再注册输出型钩子
        $plug = config('plug');
        if (isset($plug['test'])) {
            $data = $plug['test']['config'];
            if (getint($data['state']) == 0) { return; }

            cms_hook::reg('header', $this, 'css');
            cms_hook::reg('footer', $this, 'show');
        }
    }

    // 路由钩子:自定义 URL → 控制器
    function route(&$route)
    {
        $route = array_merge($route, [
            'testplug' => 'plug/test/index/index',
        ]);
    }

    function css() { echo '<style>body{background:#f30;}</style>'; }

    function show(&$id)
    {
        echo 'show plug test';
    }
}

注册三要素: 钩子名 → 当前类 $this → 方法名。第 2、3 个参数看着多余,其实是让系统在触发时能回调到你这个实例上。

已确认的钩子只有 3 个:

钩子 时机 参数
route 路由解析 &$route(引用)
header <head> 输出前 无
footer 页面底部 &$id(引用)

这是全套文档最大的空白。 想挂到"内容详情页"或"下单成功后",官方根本没告诉你钩子叫什么。

我的处理办法(也是我要给官方的建议):去 class/ 目录下搜 cms_hook::reg 的所有调用点,自建一份钩子字典。这是二开前必须做的第一步调研,否则你会花半天找一个不存在的钩子。

5.4 保存配置的标准四步(漏一步就出 bug)

php 复制代码
function Index()
{
    if (IS_POST) {
        // 1. 过滤入参
        $state = getint(F('state'));
        $db = ['state' => $state];

        // 2. 写入插件配置
        $this->db->update('cms_plug', "root='test'", ['config' => E($db)]);

        // 3. 清插件缓存  ← 最常漏的一步
        cms_cache::del('plug');

        // 4. 返回
        $this->success('保存成功');
    }
}

第 3 步漏了会怎样? 后台显示"保存成功",前台读到的还是旧值。你会怀疑人生,去查数据库,发现数据是对的------因为缓存没清。

5.5 一个隐蔽的概念混淆

cms_plug 表里有 state 字段(1 启用 / 0 禁用),示范插件又在自己的 config JSON 里存了一个 state。

这两个 state 不是一回事:

字段 含义
cms_plug.state 系统层面的插件装载状态
config.state 插件自己的业务开关

混用就会出现**"后台显示已启用,前台不生效"**。这个 bug 我调过,非常反直觉。

5.6 插件后台模板的 5 条规范

php 复制代码
<?php if(!defined('IN_CMS')) exit;?><!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{WEB_ROOT}public/css/ui.css">
...
</head>
<body>
    <div class="position">当前位置:应用管理 > <a href="{$plug}">插件管理</a> > <a href="{U('index')}">测试插件</a></div>
    <form class="ui-form" method="post">
        
        <input type="hidden" name="token" value="{$token}">
        <button type="submit" class="ui-btn blue">保存设置</button>
        <a href="{$plug}" class="ui-btn">返回</a>
    </form>
</body>
</html>
项 要求 原因
首行 if(!defined('IN_CMS')) exit; 防止模板被 URL 直接访问
资源 {WEB_ROOT}public/... 路径可移植
链接 {U('方法名')} 插件内路由
返回 {$plug} 系统注入的管理地址
表单 必须有 token 官方安全红线

注意 :插件模板用的是 {if $state==1} checked{/if} 这种更接近原生 PHP 的写法,和前台模板的 {cms:rs} 体系不是一套解析器。两套语法别混抄,抄错了不报错,就是不生效。

5.7 发布前自测清单

  • 全新环境导入 → 表建成功
  • 重复导入 → 不报错
  • 卸载 → 表清干净
  • 卸载后重装 → 数据不残留冲突
  • hook=1 但未开伪静态 → 有明确提示
  • 关掉业务开关 → 前台零输出

六、数据库规范(从 38 张表反推)

官方给了完整建表语句,但没给"怎么写表"的规范。我从 38 张表里反推出 6 条约定:

约定 规则
前缀 全表 cms_
插件表 cms_plug_插件名
引擎 MyISAM(无事务,这点很要命)
字符集 utf8mb4
时间字段 bigint Unix 时间戳,不是 datetime
软删除 使用 isshow / state 状态位
排序 ordnum(人工)+ id desc(时序)

MyISAM 无事务是重要风险 :插件里的"扣款 + 发券"必须自己写补偿逻辑,begin/commit 是无效的。

时间戳是 bigint 这点也要注意------直接 SELECT 出来是 1721974764 这种数字,模板里得用 {date('Y-m-d', $createdate)} 转换。

核心表关系:

复制代码
cms_class(栏目树,followid→cateid,0=顶级)
   └─1:N─ cms_show(内容主表)
              └─1:1─ cms_data(内容副表)
                        cid = cms_show.id

回到第二节的坑一------这张图就是那个坑的根源。


七、文档没写、但你必须知道的 6 件事

整理完发现的空白清单:

# 空白 影响 我的建议
1 Hook 钩子只有 3 个 复杂插件无从下手 搜 cms_hook::reg 调用点自建字典
2 _theme.php 字段未公开 模板配置易写错 复制默认模板,只改值不改键
3 PlugController 无接口文档 后台能力不可知 读 class/ 基类源码
4 无升级兼容说明 客户升级后插件可能失效 插件内绝不覆盖核心文件
5 无 {cms:rp} 参数表 只能靠示例倒推 按 cms:rs 参数类比 + 实测
6 无命名总则 多人协作不一致 沿用「bigint 时间戳 + isshow 状态位 + ordnum 排序」三约定

如果你在维护一个开源项目 ,这三条补上,二开沟通成本能砍掉一半:Hook 清单、_theme.php 字段表、PlugController API。


八、可以直接抄的落地检查清单

模板交付

  • _config.php / _note.php / _theme.php / theme.jpg(390px) 齐全
  • 全部文件 UTF-8 无 BOM
  • 资源路径全用 {WEB_ROOT} / {WEB_THEME}
  • 首页/列表/内容/单页/搜索/标签页全部可访问
  • 子目录部署下样式正常
  • 自定义字段能显示(走 cms_data)
  • 分页正常(列表 $showpage、内容 pagelist())
  • 空数据有 {rs:eof}
  • 含 {} 的 JS 已包 {no}
  • 响应式 or mobile/ 二选一

二次开发交付

  • 未修改 class/、app/app.php、app/cms.php
  • 自定义函数集中在 app/fun.php
  • 所有 POST 表单含 {$token}
  • 所有入参过 F() + getint()/enhtml()
  • SQL 条件无未过滤拼接
  • 上线前错误提示已关闭

插件交付

  • 目录名纯英文无特殊字符
  • 12 字段完整,config 是 JSON 字符串
  • SQL 用 @@@@ 分隔且幂等
  • hook.php 类名 = plug_插件名
  • 控制器继承 PlugController + isstall() 检查
  • 后台控制器有 check_login()
  • 保存配置后调 cms_cache::del('plug')
  • 模板首行有 IN_CMS 守卫,表单有 token
  • 关闭业务开关后前台零输出

结语

这套 CMS 的开发模型,一句话就能概括:

模板 = HTML + 标签 ({cms:rs} 查库 + {$var} 取值 + {if}/{loop} 控制)

二开 = $this->db 操作数据 + $this->tp 渲染 + app/fun.php 沉淀函数

插件 = 一个目录 + _config.php 注册 + hook.php 挂载 + 控制器/模板

三条线共同的硬约束:UTF-8、{WEB_ROOT}、Token、F() 过滤、cms_ 前缀、不碰核心文件。

最后说一个方法论上的收获:当文档写得薄的时候,不要抱怨,去读示范代码。 官方示范插件那 6 个文件里藏的信息量,比 27 篇文档加起来还多------因为它是一份能跑通的最小可运行示例。规范的真相永远在可运行的代码里。


本文基于能鈺CMS 帮助文档(help.nengyu.com,27 篇)与官方示范插件包整理,标注「推断」的内容建议实测确认。

相关推荐
miofly2 小时前
GitHub 日榜趋势速报 | 2026-10-27
开源·github
冬奇Lab11 小时前
一天一个开源项目(第248篇):CleanYourTV —— 让 AI 编程助手通过 Wi-Fi 给安卓电视去广告、换上 Apple TV 风格主屏
开源·资讯
wflynn12 小时前
GitHub 今日推荐|gpu-unlock-ada:Ada 显卡张量算力翻倍解锁
开源·github·c
SL_staff14 小时前
JVS私有化文档系统上线避坑指南:5大运维与业务协同断点及检测加固实践
java·开源·全栈
漫步的猪16 小时前
# 告别混乱与付费墙!Linkit:完全免费、本地优先的开源 AI 智能书签与收藏夹管理神器
人工智能·ai·开源·react·免费·书签·收藏夹
miofly16 小时前
NVIDIA RTX Spark 编码代理性能与本地 AI 开发机配置
开源·github
wflynn16 小时前
GitHub 周榜趋势速报 | 2026-10-25
开源·github
孟健16 小时前
Kimi K3.1 Pro 编程实测:3个月27万行代码,每年1.7万预算下如何9分钟修好顽固Bug
前端·开源·ai编程
sbjdhjd16 小时前
AI产业周度动态:从“更强模型”走向“可执行、可治理、可计价”的系统竞争 (2026年10月19日—10月25日)
人工智能·笔记·ai·开源·gemini·信息差·claude code