苹果cms 站群,留言分站管理

苹果cms多域名站群的需求非常常见。默认情况下,站群模式虽然可以轻松展示不同域名,但后台留言、播放进度、用户数据完全共享,容易暴露所有站点都使用同一套数据库,这在 SEO 优化和用户体验上都有一定局限。

本文将分享一套完整方案,实现 多站点留言表独立存储,支持前台留言和后台管理按站点切换,既方便维护,又能在一定程度上降低站群数据混乱的风险。

接下来,我会按照 数据库表复制、模型和控制器修改、后台前台展示调整 的顺序进行讲解,让你一步步实现功能。

注:在开始之前请备份数据库和需要修改的文件

本文以域名 aaa.probbb.pro 作为示例

主站  aaa.pro  留言存储表:mac_gbook 
子站  bbb.pro 留言存储表:mac_gbook_bbb

代码中不用填写表前缀 mac_

数据库表准备

  • 复制默认留言表 mac_gbookmac_gbook_bbb
  • 保留字段一致,保证模型操作兼容
  • 执行 SQL 示例:
CREATE TABLE `mac_gbook_bbb` LIKE `mac_gbook`;

修改模型(Gbook.php)

文件路径:

application/common/model/Gbook.php

完整代码

<?php
namespace app\common\model;
use think\Db;
use think\Request;

class Gbook extends Base {
    // 设置数据表(不含前缀)
    protected $name = 'gbook';

    // 定义时间戳字段名
    protected $createTime = '';
    protected $updateTime = '';

    // 自动完成
    protected $auto       = [];
    protected $insert     = [];
    protected $update     = [];

    // 初始化方法,动态设置表名(用于前端提交)
    public function initialize()
    {
        parent::initialize();
        // 修复:用 request()->host() 更可靠
        $host = Request::instance()->host();
        $domain_to_table = [
            'aaa.pro'     => 'gbook',
            'www.aaa.pro' => 'gbook',
            'bbb.pro'     => 'gbook_bbb',
            'www.bbb.pro' => 'gbook_bbb',
        ];
        $this->name = $domain_to_table[$host] ?? 'gbook';
        // 可选调试:error_log("Host: $host, Table: " . $this->name); // 生产移除
    }

    // 静态方法,根据站点返回表名(用于后台跨表)
    public static function getTableBySite($site)
    {
        $domain_to_table = [
            'all'            => null,
            'aaa'         => 'gbook',
            'bbb'         => 'gbook_bbb',
        ];
        return $domain_to_table[$site] ?? 'gbook';
    }

    public function getGbookStatusTextAttr($val,$data)
    {
        $arr = [0=>lang('disable'),1=>lang('enable')];
        return $arr[$data['gbook_status']];
    }

    public function listData($where,$order,$page=1,$limit=20,$start=0, $table = null)
    {
        if(!is_array($where)){
            $where = json_decode($where,true);
        }
        $queryTable = $table ?: $this->name;  // 不含前缀
        $total = Db::name($queryTable)->where($where)->count();
        $limit_str = ($limit * ($page-1) + $start) .",".$limit;
        $list = Db::name($queryTable)->where($where)->order($order)->limit($limit_str)->select();
        foreach ($list as $k=>$v){
            $list[$k]['user_portrait'] = mac_get_user_portrait($v['user_id']);
            $list[$k]['gbook_content'] = mac_restore_htmlfilter($list[$k]['gbook_content']);
            $list[$k]['gbook_reply'] = mac_restore_htmlfilter($list[$k]['gbook_reply']);
        }
        return ['code'=>1,'msg'=>lang('data_list'),'page'=>$page,'limit'=>$limit,'total'=>$total,'list'=>$list];
    }

    public function listCacheData($lp)
    {
        if (!is_array($lp)) {
            $lp = json_decode($lp, true);
        }

        $order = $lp['order'];
        $by = $lp['by'];
        $paging = $lp['paging'];
        $start = intval(abs($lp['start']));
        $num = intval(abs($lp['num']));
        $rid = intval(abs($lp['rid']));
        $uid = intval(abs($lp['uid']));
        $half = intval(abs($lp['half']));
        $pageurl = $lp['pageurl'];
        $page = 1;
        $where = [];

        if(empty($num)){
            $num = 20;
        }
        if($start>1){
            $start--;
        }
        if (!in_array($paging, ['yes', 'no'])) {
            $paging = 'no';
        }

        if($paging=='yes') {
            $param = mac_param_url();
            if(!empty($param['rid'])){
                $rid = $param['rid'];
            }
            if(!empty($param['by'])){
                $by = $param['by'];
            }
            if(!empty($param['order'])){
                $order = $param['order'];
            }
            if(!empty($param['page'])){
                $page = intval($param['page']);
            }

            foreach($param as $k=>$v){
                if(empty($v)){
                    unset($param[$k]);
                }
            }
            if(empty($pageurl)){
                $pageurl = 'gbook/index';
            }
            $param['page'] = 'PAGELINK';
            $pageurl = mac_url($pageurl,$param);
        }

        $where['gbook_status'] = ['eq',1];
        if(!empty($rid)){
            $where['gbook_rid'] = ['eq',$rid];
        }
        if(!empty($uid)){
            $where['user_id'] = ['eq',$uid];
        }
        if(!in_array($by, ['id', 'time','reply_time'])) {
            $by = 'time';
        }
        if(!in_array($order, ['asc', 'desc'])) {
            $order = 'desc';
        }
        $order= 'gbook_'.$by .' ' . $order;


        $cach_name = $GLOBALS['config']['app']['cache_flag']. '_' .md5('gbook_listcache_'.join('&',$where).'_'.$order.'_'.$page.'_'.$num.'_'.$start);

        $res = $this->listData($where,$order,$page,$num,$start);
        $res['pageurl'] = $pageurl;
        $res['half'] = $half;
        return $res;

    }

    public function infoData($where,$field='*', $table = null)
    {
        if(empty($where) || !is_array($where)){
            return ['code'=>1001,'msg'=>lang('param_err')];
        }
        $queryTable = $table ?: $this->name;  // 不含前缀
        $info = Db::name($queryTable)->field($field)->where($where)->find();

        if(empty($info)){
            return ['code'=>1002,'msg'=>lang('obtain_err')];
        }

        return ['code'=>1,'msg'=>lang('obtain_ok'),'info'=>$info];
    }

    public function saveData($data, $table = null)
    {   
        $validate = \think\Loader::validate('Gbook');
        if(!$validate->check($data)){
            return ['code'=>1001,'msg'=>lang('param_err').':'.$validate->getError() ];
        }
        
        // xss过滤
        $filter_fields = [
            'gbook_name',
            'gbook_content',
            'gbook_reply',
        ];
        foreach ($filter_fields as $filter_field) {
            if (!isset($data[$filter_field])) {
                continue;
            }
            $data[$filter_field] = mac_filter_xss($data[$filter_field]);
        }

        $queryTable = $table ?: $this->name;  // 不含前缀

        if(!empty($data['gbook_id'])){
            if(!empty($data['gbook_reply'])){
                $data['gbook_reply_time'] = time();
            }
            $where=[];
            $where['gbook_id'] = ['eq',$data['gbook_id']];
            $res = Db::name($queryTable)->where($where)->update($data);
        }
        else{
            $data['gbook_time'] = time();
            $res = Db::name($queryTable)->insert($data);
        }
        if(false === $res){
            return ['code'=>1002,'msg'=>lang('save_err').':'.$this->getError() ];
        }
        return ['code'=>1,'msg'=>lang('save_ok')];
    }

    public function delData($where, $table = null)
    {
        $queryTable = $table ?: $this->name;  // 不含前缀
        $res = Db::name($queryTable)->where($where)->delete();
        if($res===false){
            return ['code'=>1001,'msg'=>lang('del_err').':'.$this->getError() ];
        }
        return ['code'=>1,'msg'=>lang('del_ok')];
    }

    public function fieldData($where,$col,$val, $table = null)
    {
        if(!isset($col) || !isset($val)){
            return ['code'=>1001,'msg'=>lang('param_err')];
        }

        $data = [];
        $data[$col] = $val;
        $queryTable = $table ?: $this->name;  // 不含前缀
        $res = Db::name($queryTable)->where($where)->update($data);
        if($res===false){
            return ['code'=>1001,'msg'=>lang('set_err').':'.$this->getError() ];
        }
        return ['code'=>1,'msg'=>lang('set_ok')];
    }
}

修改后台控制器(Gbook.php)

文件路径:

application/admin/controller/Gbook.php

完整代码

<?php
namespace app\admin\controller;
use think\Db;

class Gbook extends Base
{
    public function __construct()
    {
        parent::__construct();
        // 后台统一用默认表,后续方法中覆盖
    }

    // 获取站点列表
    private function getSiteList()
    {
        return [
            'all'    => '所有站点',
            'aaa' => '主站 (aaa.pro)',
            'bbb' => '子站 (bbb.pro)',
        ];
    }

    // 辅助方法,数组排序
    private function array_sort($array, $keys, $type='asc')
    {
        $keysvalue = [];
        foreach ($array as $k => $v) {
            $keysvalue[$k] = $v[$keys];
        }
        if ($type == 'asc') {
            asort($keysvalue);
        } else {
            arsort($keysvalue);
        }
        reset($keysvalue);
        foreach ($keysvalue as $k => $v) {
            $return_array[$k] = $array[$k];
        }
        return $return_array;
    }

    public function data()
    {
        $param = input();
        $param['page'] = intval($param['page']) <1 ? 1 : $param['page'];
        $param['limit'] = intval($param['limit']) <1 ? $this->_pagesize : $param['limit'];
        $param['site'] = $param['site'] ?? 'all';

        $where = [];
        if(in_array($param['status'],['0','1'],true)){
            $where['gbook_status'] = ['eq',$param['status']];
        }
        if(in_array($param['reply'],['1','2'])){
            if($param['reply'] == 2){
                $where['gbook_reply_time'] = ['gt', 0];
            }
        }
        if(in_array($param['genre'],['1','2'])){
            if($param['genre'] == 1){
                $where['gbook_rid'] = 0;
            } elseif($param['genre'] ==2){
                $where['gbook_rid'] = ['gt',0];
            }
        }
        if(!empty($param['uid'])){
            $where['user_id'] = ['eq',$param['uid'] ];
        }
        if(!empty($param['wd'])){
            $param['wd'] = htmlspecialchars(urldecode($param['wd']));
            $where['gbook_name|gbook_content'] = ['like','%'.$param['wd'].'%'];
        }

        $order = 'gbook_time desc';
        $siteList = $this->getSiteList();
        $tableName = model('Gbook')->getTableBySite($param['site']);

        if ($param['site'] == 'all' || $tableName === null) {
            $allList = [];
            $total = 0;
            foreach ($siteList as $siteKey => $siteName) {
                if ($siteKey == 'all') continue;
                $tempTableNoPrefix = model('Gbook')->getTableBySite($siteKey);
                if (empty($tempTableNoPrefix)) continue;
                
                $tempTotal = Db::name($tempTableNoPrefix)->where($where)->count();
                $total += $tempTotal;
                
                $tempList = Db::name($tempTableNoPrefix)->where($where)->order($order)->select();
                $tempList = $tempList ?: [];
                foreach ($tempList as &$v) {
                    $v['site'] = $siteName;
                    $v['table'] = $tempTableNoPrefix;
                    $v['user_portrait'] = mac_get_user_portrait($v['user_id']);
                    $v['gbook_content'] = mac_restore_htmlfilter($v['gbook_content']);
                    $v['gbook_reply'] = mac_restore_htmlfilter($v['gbook_reply']);
                }
                $allList = array_merge($allList, $tempList);
            }
            $allList = $this->array_sort($allList, 'gbook_time', 'desc');
            $list = array_slice($allList, ($param['page']-1)*$param['limit'], $param['limit']);
            $res = ['code'=>1,'msg'=>lang('data_list'),'page'=>$param['page'],'limit'=>$param['limit'],'total'=>$total,'list'=>$list];
        } else {
            // 单表:用 listData with $table (不含前缀)
            $res = model('Gbook')->listData($where, $order, $param['page'], $param['limit'], 0, $tableName);
            foreach ($res['list'] as &$v) {
                $v['site'] = $siteList[$param['site']] ?? $param['site'];
                $v['table'] = $tableName;
            }
        }

        $this->assign('list',$res['list']);
        $this->assign('total',$res['total']);
        $this->assign('page',$res['page']);
        $this->assign('limit',$res['limit']);
        $this->assign('siteList', $siteList);
        $this->assign('curSite', $param['site']);

        $param['page'] = '{page}';
        $param['limit'] = '{limit}';
        $this->assign('param',$param);
        $this->assign('title',lang('admin/gbook/title'));
        return $this->fetch('admin@gbook/index');
    }

    public function info()
    {
        if (Request()->isPost()) {
            $param = input();
            $tableName = $param['table'] ?? '';
            $res = model('Gbook')->saveData($param, $tableName);
            if($res['code']>1){
                return $this->error($res['msg']);
            }
            return $this->success($res['msg']);
        }

        $id = input('id');
        $table = input('table', '');
        $where=[];
        $where['gbook_id'] = ['eq',$id];
        $res = model('Gbook')->infoData($where, '*', $table);

        $this->assign('info',$res['info']);
        $this->assign('table', $table);
        $this->assign('title',lang('admin/gbook/title'));
        return $this->fetch('admin@gbook/info');
    }

    public function del()
    {
        $param = input();
        $ids = $param['ids'];
        $all = $param['all'];
        $table = $param['table'] ?? '';  // 从 POST/GET 获取
        $site = $param['site'] ?? '';   // 从 POST/GET 获取 site

        // 修复:如果 site 为空,从 Referer 解析
        if (empty($site)) {
            $referer = $_SERVER['HTTP_REFERER'] ?? '';
            if (!empty($referer)) {
                $parsed_url = parse_url($referer);
                parse_str($parsed_url['query'] ?? '', $query_params);
                $site = $query_params['site'] ?? '';
            }
        }

        // 推断 table
        if (empty($table) && !empty($site)) {
            $table = model('Gbook')->getTableBySite($site);
        }

        if(!empty($ids) || !empty($all)){
            $where=[];
            $where['gbook_id'] = ['in',$ids];
            if($all==1){
                $where['gbook_id'] = ['gt',0];
            }
            $res = model('Gbook')->delData($where, $table);
            if($res['code']>1){
                return $this->error($res['msg']);
            }
            return $this->success($res['msg']);
        }
        return $this->error(lang('param_err'));
    }

    public function field()
    {
        $param = input();
        $ids = $param['ids'];
        $col = $param['col'];
        $val = $param['val'];
        $table = $param['table'] ?? '';  // 从 POST/GET 获取
        $site = $param['site'] ?? '';   // 从 POST/GET 获取 site

        // 修复:如果 site 为空,从 Referer 解析
        if (empty($site)) {
            $referer = $_SERVER['HTTP_REFERER'] ?? '';
            if (!empty($referer)) {
                $parsed_url = parse_url($referer);
                parse_str($parsed_url['query'] ?? '', $query_params);
                $site = $query_params['site'] ?? '';
            }
        }

        // 推断 table
        if (empty($table) && !empty($site)) {
            $table = model('Gbook')->getTableBySite($site);
        }

        if(!empty($ids) && in_array($col,['gbook_status']) ){
            $where=[];
            $where['gbook_id'] = ['in',$ids];

            $res = model('Gbook')->fieldData($where,$col,$val, $table);
            if($res['code']>1){
                return $this->error($res['msg']);
            }
            return $this->success($res['msg']);
        }
        return $this->error(lang('param_err'));
    }
}

修改后台留言管理模板

后台默认入口是

application/admin/view/gbook/index.html

完整代码

{include file="../../../application/admin/view/public/head" /}
<div class="page-container p10">

    <div class="my-toolbar-box" >
        <div class="center mb10">
            <form class="layui-form " method="post" action="{:url('data')}">
                <!-- 站点选择下拉框 -->
                <div class="layui-input-inline w120">
                    <select name="site">
                        <option value="">{:lang('select_site')}</option>
                        {volist name="siteList" id="vo"}
                        <option value="{$key}" {if condition="$key eq $curSite"}selected{/if}>{$vo}</option>
                        {/volist}
                    </select>
                </div>
                
                <div class="layui-input-inline w100">
                    <select name="status">
                        <option value="">{:lang('select_status')}</option>
                        <option value="0" {if condition="$param['status'] == '0'"}selected {/if}>{:lang('reviewed_not')}</option>
                        <option value="1" {if condition="$param['status'] == '1'"}selected {/if}>{:lang('reviewed')}</option>
                    </select>
                </div>
                <!-- 回复状态 name="reply" -->
                <div class="layui-input-inline w100">
                    <select name="reply">
                        <option value="">{:lang('select_reply_status')}</option>
                        <option value="1" {if condition="$param['reply'] eq '1'"}selected {/if}>{:lang('reply_not')}</option>
                        <option value="2" {if condition="$param['reply'] eq '2'"}selected {/if}>{:lang('reply_yes')}</option>
                    </select>
                </div>
                <!-- 类型 name="genre" -->
                <div class="layui-input-inline w100">
                    <select name="genre">
                        <option value="">{:lang('select_genre')}</option>
                        <option value="1" {if condition="$param['genre'] eq '1'"}selected {/if}>{:lang('gbook')}</option>
                        <option value="2" {if condition="$param['genre'] eq '2'"}selected {/if}>{:lang('report')}</option>
                    </select>
                </div>
                <div class="layui-input-inline">
                    <input type="text" autocomplete="off" placeholder="{:lang('wd')}" class="layui-input" name="wd" value="{$param['wd']|mac_filter_xss}">
                </div>
                <button class="layui-btn mgl-20 j-search" >{:lang('btn_search')}</button>
            </form>
        </div>
        <div class="layui-btn-group">
            <!-- 批量删除按钮:URL 已带 site,但隐藏字段确保 POST 带 -->
            <a data-href="{:url('del')}?site={$curSite}" class="layui-btn layui-btn-primary j-page-btns confirm"><i class="layui-icon"></i>{:lang('del')}</a>
            <!-- 批量状态按钮:URL 已带 site,但隐藏字段确保 POST 带 -->
            <a data-href="{:url('index/select')}?tab=gbook&col=gbook_status&tpl=select_status&url=gbook/field&site={$curSite}" data-width="470" data-height="100" data-checkbox="1" class="layui-btn layui-btn-primary j-select"><i class="layui-icon"></i>{:lang('status')}</a>
            <!-- 清空按钮:URL 已带 site -->
            <a data-href="{:url('del')}?all=1&site={$curSite}" class="layui-btn layui-btn-primary j-ajax" confirm="{:lang('clear_confirm')}"><i class="layui-icon"></i>{:lang('clear')}</a>
        </div>
    </div>

    <!-- 修复:表单添加隐藏 site 字段,确保批量 POST 带 site 参数 -->
    <form class="layui-form" method="post" id="pageListForm" >
        <input type="hidden" name="site" value="{$curSite}">
        <table class="layui-table" lay-size="sm">
        <thead>
        <tr>
            <th width="25"><input type="checkbox" lay-skin="primary" lay-filter="allChoose"></th>
            <th width="60">{:lang('id')}</th>
            <th width="80">站点</th>
            <th width="60">{:lang('status')}</th>
            <th width="60">{:lang('genre')}</th>
            <th >{:lang('gbook')}</th>
            <th >{:lang('report')}</th>
            <th width="100">{:lang('opt')}</th>
        </tr>
        </thead>

        {volist name="list" id="vo"}
        <tr>
            <td><input type="checkbox" name="ids[]" value="{$vo.gbook_id}" class="layui-checkbox checkbox-ids" lay-skin="primary"></td>
            <td>{$vo.gbook_id}</td>
            <td>{$vo.site}</td>
            <td>{if condition="$vo.gbook_status eq 0"}<span class="layui-badge">{:lang('reviewed_not')}</span>{else}<span class="layui-badge layui-bg-green">{:lang('reviewed')}</span>{/if}</td>
            <td>{if condition="$vo.gbook_rid eq 0"}{:lang('gbook')}{else/}{:lang('report')}{/if}</td>
            <td>
                <div class="c-999 f-12">
                    <u style="cursor:pointer" class="text-primary">{$vo.gbook_name|htmlspecialchars}:</u>
                    <time>【{$vo.gbook_time|mac_day='color'}】</time>
                    <span class="ml-20">ip:【{$vo.gbook_ip|long2ip}】</span>
                </div>
                <div class="f-12 c-999">
                    <span class="ml-20">{:lang('status')}:</span>
                    {:lang('gbook')}:{$vo.gbook_content|htmlspecialchars}
                </div>
            </td>
            <td>
                <div class="c-999 f-12">
                    {:lang('reply_time')}:{$vo.gbook_reply_time|mac_day='color'}
                </div>
                <div class="f-12 c-999">
                    {:lang('reply')}:{$vo.gbook_reply|htmlspecialchars}
                </div>
                <div> </div>
            </td>
            <td>
                <a class="layui-badge-rim j-iframe" data-href="{:url('info?id='.$vo['gbook_id'].'&table='.$vo['table'])}" href="javascript:;" title="{:lang('reply')}">{:lang('reply')}</a>
                <a class="layui-badge-rim j-tr-del" data-href="{:url('del?ids='.$vo['gbook_id'].'&table='.$vo['table'])}" href="javascript:;" title="{:lang('del')}">{:lang('del')}</a>
            </td>
        </tr>
        {/volist}
        </tbody>
        </table>

        <div id="pages" class="center"></div>

    </form>
</div>

{include file="../../../application/admin/view/public/foot" /}

<script type="text/javascript">
    var curUrl="{:url('gbook/data',$param)}";
    layui.use(['laypage', 'layer','form'], function() {
        var laypage = layui.laypage
                , layer = layui.layer,
                form = layui.form;

        laypage.render({
            elem: 'pages'
            ,count: {$total}
            ,limit: {$limit}
            ,curr: {$page}
            ,layout: ['count', 'prev', 'page', 'next', 'limit', 'skip']
            ,jump: function(obj,first){
                if(!first){
                    location.href = curUrl.replace('%7Bpage%7D',obj.curr).replace('%7Blimit%7D',obj.limit);
                }
            }
        });


    });
</script>
</body>
</html>

前台查看不同站点留言板内容是否已经更改,此时 bbb.pro 留言板应该为空,再通过不同网站提交留言测试,后台留言管理查看是否正常。

发表评论