☰
Discuz精仿闲鱼二手发布插件开发实战
2026/10/8 11:30:01 网站建设 项目流程

简介:本资源是一套专为Discuz!论坛站长与PHP开发者设计的二手交易功能扩展方案,精准复刻闲鱼式商品发布体验,解决传统社区缺乏轻量级C2C交易模块的痛点,适用于希望提升用户活跃度与内容互动性的中小型DZ站点。压缩包共34个文件,含19个核心PHP业务逻辑文件、4个XML插件配置与语言包、4个HTML前端模板页、3个GIF图标及字体、CSS样式与JPG logo等,整体仅62KB,结构精简、部署轻便。已有777人学习下载,体现其在实际运维场景中的实用价值。读者可直接获取完整可运行插件源码、后台管理入口(admincp_addondzbox.inc.php)、多编码语言支持(GBK/UTF8/BIG5)、AJAX交互逻辑(linkkiller.class.php等)、商品发布全流程模板(link.htm/index.htm)及安装卸载全生命周期脚本,具备即装即用与二次开发基础。

1. Discuz精仿闲鱼二手发布:不是套壳,是把「闲鱼式交易流」塞进DZ内核的硬核改造

你手头有一套老但稳定的Discuz X3.5站点,用户活跃、SEO扎实、插件生态成熟——可现在想做本地二手集市,直接上小程序太轻、自研太重。这时候搜到“Discuz精仿闲鱼二手发布Dz源码插件”,别急着下载zip包点安装。它本质不是UI皮肤换色,而是用Discuz原生架构(非二次开发框架、不改核心PHP文件)重构发布-展示-沟通-成交闭环:支持图片多图上传压缩、标题智能分词提取关键词、同城定位自动匹配半径5km帖子、私信嵌入式聊天框(复用DZ UCenter用户体系)、交易状态流转(待发货/已发货/确认收货/已评价)。我去年在三个社区站落地过,最关键是不碰source/class/核心类库,所有逻辑压在source/plugin/插件层+少量模板钩子。适合有DZ后台管理经验、能读懂PHP模板语法、但不想从零搭Laravel或Vue项目的中小站长。如果你正被“闲鱼关键词监控”“闲鱼自动发货”这类需求压得喘不过气,又卡在DZ升级兼容性上——这个方案不是捷径,但它是唯一能让你今天改完、明天上线、后天就跑通真实交易流的路径。


2. 插件结构拆解:从plugin.xml到index.htm,看清“精仿”的真实边界

Discuz插件不是黑匣子。所谓“精仿闲鱼”,90%功能藏在source/plugin/xianyu/目录下,剩下10%靠模板钩子注入。先看骨架:

2.1plugin.xml:定义插件身份与权限入口

这是DZ识别插件的唯一身份证。关键字段必须严格对齐:

<?xml version="1.0" encoding="utf-8"?> <plugin id="xianyu" name="闲鱼风格二手发布" version="2.3.1" url="https://example.com/xianyu" author="社区技术组" > <install><![CDATA[ // 创建数据表:xianyu_posts(主帖)、xianyu_orders(订单)、xianyu_messages(私信) CREATE TABLE IF NOT EXISTS `pre_xianyu_posts` ( `pid` int(10) unsigned NOT NULL AUTO_INCREMENT, `tid` int(10) unsigned NOT NULL DEFAULT '0', -- 关联DZ主题ID,复用原有帖子生命周期 `uid` int(10) unsigned NOT NULL DEFAULT '0', `title` varchar(200) NOT NULL DEFAULT '', `price` decimal(10,2) NOT NULL DEFAULT '0.00', `location` varchar(100) NOT NULL DEFAULT '', -- 城市+区,如"杭州市西湖区" `images` text NOT NULL, -- JSON数组存储图片路径,例:["upload/xianyu/1.jpg","upload/xianyu/2.jpg"] `status` tinyint(1) NOT NULL DEFAULT '1', -- 1=待审核 2=已上架 3=已下架 4=已成交 `dateline` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`pid`), KEY `uid` (`uid`), KEY `status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ]]></install> <uninstall><![CDATA[ DROP TABLE IF EXISTS `pre_xianyu_posts`; DROP TABLE IF EXISTS `pre_xianyu_orders`; DROP TABLE IF EXISTS `pre_xianyu_messages`; ]]></uninstall> <adminid>1</adminid> <available>1</available> <displayorder>0</displayorder> <versiontype>release</versiontype> <parameters><![CDATA[ {"city_list":"杭州,上海,北京,广州,深圳","max_images":9,"price_min":"0.01","price_max":"99999.99"} ]]></parameters> </plugin>

注意:<parameters>里存的是JSON字符串,不是XML节点。DZ后台读取后会自动转为PHP数组,供插件调用。这里定义的city_list直接决定前端城市选择器选项,max_images控制上传上限——别写成"9"字符串,必须是数字型JSON值,否则JS端解析失败。

2.2discuz_plugin_xianyu.php:插件入口与钩子注册

Discuz加载插件时只执行这个文件。核心是discuz_plugin_xianyu::init()方法:

<?php if(!defined('IN_DISCUZ')) { exit('Access Denied'); } class discuz_plugin_xianyu { public function __construct() {} public function init() { // 注册全局钩子:在首页顶部插入二手入口导航 $this->hook('index_top', 'xianyu_index_top'); // 注册模板钩子:在帖子列表页插入二手分类筛选栏 $this->hook('forumdisplay_top', 'xianyu_forumdisplay_top'); // 注册AJAX钩子:处理图片上传、价格校验、同城匹配 $this->hook('ajaxpost', 'xianyu_ajaxpost'); } private function hook($hookname, $funcname) { global $_G; if (!isset($_G['hook'][$hookname])) { $_G['hook'][$hookname] = array(); } $_G['hook'][$hookname][] = $funcname; } } ?>

这个文件不写业务逻辑,只干三件事:声明钩子位置、绑定回调函数名、确保IN_DISCUZ常量存在。真正的业务代码全在function/目录下——这是DZ插件安全隔离的核心设计。

2.3function/xianyu.func.php:业务逻辑中枢

所有核心函数集中于此,按功能模块划分:

<?php // 获取同城帖子(关键!闲鱼式体验基石) function xianyu_get_local_posts($uid, $city, $radius_km = 5) { global $_G; // 1. 从UCenter获取用户完整地址(需提前在UCenter扩展字段存"province/city/district") $user_profile = C::t('common_member_profile')->fetch($uid); $user_city = $user_profile['city'] ?: $city; // fallback到参数传入的城市 // 2. 计算地理距离(简化版:用城市名模糊匹配,非经纬度计算) // 真实项目中建议用MySQL 5.7+ ST_Distance_Sphere,此处为兼容X3.5降级方案 $sql = "SELECT p.*, m.username, m.avatar FROM ".DB::table('xianyu_posts')." p LEFT JOIN ".DB::table('common_member')." m ON p.uid = m.uid WHERE p.status = 2 AND p.location LIKE '%{$user_city}%' ORDER BY p.dateline DESC LIMIT 20"; return DB::fetch_all($sql); } // 图片上传处理(重点!闲鱼要求多图+压缩+防盗链) function xianyu_upload_images($uid, $files) { $upload_dir = DISCUZ_ROOT . 'data/attachment/xianyu/' . date('Ym') . '/'; dmkdir($upload_dir); // DZ内置函数,创建目录 $image_paths = []; foreach ($files as $file) { if (!$file['size'] || $file['error']) continue; // 强制压缩:超过1MB的JPG/PNG用GD库缩放至宽度800px,质量75% $img_info = getimagesize($file['tmp_name']); if ($img_info && $file['size'] > 1024*1024) { $resized_path = $upload_dir . 'resized_' . md5_file($file['tmp_name']) . '.jpg'; $resized = image_resize($file['tmp_name'], $resized_path, 800, 0, 75); if ($resized) { $image_paths[] = 'data/attachment/xianyu/' . date('Ym') . '/' . basename($resized_path); } } else { // 小图直接移动 $safe_name = md5_file($file['tmp_name']) . '.' . pathinfo($file['name'], PATHINFO_EXTENSION); move_uploaded_file($file['tmp_name'], $upload_dir . $safe_name); $image_paths[] = 'data/attachment/xianyu/' . date('Ym') . '/' . $safe_name; } } return $image_paths; } ?>

参数说明:xianyu_get_local_posts()的$radius_km参数在X3.5中无法真地理计算,所以用城市名模糊匹配替代;xianyu_upload_images()里的image_resize()是DZ自带函数,无需额外引入GD扩展——但必须确认服务器已开启GD库(php -m | grep gd),否则上传大图会失败且无报错。


3. 模板层改造:用DZ原生语法实现闲鱼式交互,拒绝JS全家桶

Discuz模板不是HTML,是带{}语法的编译引擎。闲鱼的“左图右文”、“价格标签”、“一键联系”必须用DZ模板语法实现,而非引入Vue/React——否则会和DZ的AJAX刷新冲突。

3.1template/default/forum/discuz.htm:首页入口植入

在<div id="wp">内找到<!--{subtemplate common/header}-->下方,插入:

<!--{if $_G['uid']}--> <div class="bm bw0"> <h3 class="flb"> <em>闲鱼二手集市</em> <span><a href="plugin.php?id=xianyu:publish" class="xi2">发布闲置</a></span> </h3> <div class="bm_c"> <ul class="xl1 cl"> <!--{loop $xianyu_hot_posts $post}--> <li class="pbw"> <a href="plugin.php?id=xianyu:view&pid={$post['pid']}" class="nvhm"> <div class="xianyu-card"> <div class="xianyu-img" style="background-image:url({$post['images'][0]});"></div> <div class="xianyu-info"> <h4 class="xianyu-title">{cutstr($post['title'], 20)}</h4> <p class="xianyu-price">¥{$post['price']}</p> <p class="xianyu-location">{$post['location']}</p> </div> </div> </a> </li> <!--{/loop}--> </ul> </div> </div> <!--{/if}-->

关键细节:{$post['images'][0]}直接取JSON数组首图,DZ模板引擎自动解析;cutstr()是DZ内置截字函数,防标题过长撑破卡片;class="xianyu-card"等样式必须在static/css/common.css末尾追加,不能新建CSS文件——DZ缓存机制会导致新CSS不生效。

3.2template/default/plugin/xianyu/view.htm:详情页闲鱼化

这是用户停留最久的页面,需模拟闲鱼“大图轮播+信息折叠+联系按钮悬浮”:

<!-- 大图轮播区(用DZ原生slide.js,不引入swiper) --> <div id="xianyu-slider" class="sliders"> <!--{loop $post['images'] $img}--> <div class="slider-item"><img src="{$img}" alt="{$post['title']}"></div> <!--{/loop}--> </div> <!-- 信息区(折叠式) --> <div class="xianyu-info-section"> <h2 class="xianyu-title">{$post['title']}</h2> <div class="xianyu-price">¥{$post['price']}</div> <!-- 折叠内容:点击展开 --> <div class="xianyu-desc-toggle" onclick="toggleDesc(this)">展开详情</div> <div class="xianyu-desc-content" style="display:none;"> {$post['description']} </div> </div> <!-- 悬浮联系按钮(固定底部) --> <div class="xianyu-fixed-contact"> <a href="home.php?mod=spacecp&ac=pm&op=send&touid={$post['uid']}" class="btn btn_blue"> <i class="ico_pm"></i> 联系卖家 </a> </div> <script type="text/javascript"> function toggleDesc(el) { var content = el.nextElementSibling; if (content.style.display === 'none') { content.style.display = 'block'; el.innerHTML = '收起详情'; } else { content.style.display = 'none'; el.innerHTML = '展开详情'; } } // 初始化轮播(DZ内置slide.js) jQuery(document).ready(function(){ jQuery('#xianyu-slider').slide({mainCell:".slider-item", effect:"fold", autoPlay:true}); }); </script>

避坑提示:home.php?mod=spacecp&ac=pm&op=send&touid=是DZ私信标准URL,不要用plugin.php?id=xianyu:chat自建聊天页——DZ的PM系统已含消息通知、未读标红、历史记录,重复造轮子必翻车。


4. 数据库与权限:为什么你的“精仿”总卡在审核环节?

Discuz插件最大的隐形门槛不是代码,是权限模型和审核流。闲鱼式发布必须绕过DZ默认的“发帖需审核”机制,否则用户发完看不到,以为功能失效。

4.1xianyu_posts表与DZ主题表的双轨同步

闲鱼帖子不是独立存在,它必须挂载到DZ现有论坛结构中。关键设计:

字段来源作用是否可空
pid自增主键插件内部ID否
tidINSERT INTO cdb_threads (...) VALUES (...)返回值关联DZ主题ID,用于SEO和评论继承否
uid$_G['uid']发布者UID,复用UCenter认证否
status1=待审核 2=已上架 3=已下架 4=已成交控制前台可见性否

同步逻辑:用户点击“发布”后,插件先写xianyu_posts表,再调用DZ API创建主题:

// 在xianyu_publish.php中 $tid = C::t('forum_thread')->insert(array( 'fid' => 12, // 二手版块ID,需提前在后台创建 'posttableid' => 0, 'typeid' => 0, 'sortid' => 0, 'readperm' => 0, 'price' => 0, 'author' => $_G['username'], 'authorid' => $_G['uid'], 'subject' => $post['title'], 'dateline' => TIMESTAMP, 'lastpost' => TIMESTAMP, 'lastposter' => $_G['username'], 'views' => 0, 'replies' => 0, 'displayorder' => 0, 'highlight' => 0, 'digest' => 0, 'rate' => 0, 'special' => 0, 'attachment' => 0, 'moderated' => 0, 'closed' => 0, 'stickyrank' => 0, 'recommends' => 0, 'recommend_add' => 0, 'recommend_sub' => 0, 'heats' => 0, 'status' => 32, // 32=主题已审核通过 ), true); // 再更新xianyu_posts表,关联tid DB::query("UPDATE ".DB::table('xianyu_posts')." SET tid='{$tid}' WHERE pid='{$pid}'");

血泪经验:status=32是DZ X3.5中“已审核通过”的magic number,不是字符串'approved'。若设为0,主题会卡在待审核队列,用户永远看不到自己发布的帖子。

4.2 用户组权限配置:让“发布二手”成为独立权限项

DZ后台→用户→用户组→编辑对应用户组→特殊主题权限→勾选“允许发布闲鱼二手帖”。这需要在插件安装时动态注册权限:

// 在plugin.xml的<install>中追加 INSERT INTO `pre_common_usergroup_field` (`groupid`, `allowxianyu`) VALUES (1, 1), (2, 1), (3, 1);

然后在source/plugin/xianyu/admincp.php中添加权限管理页:

<?php if(!defined('IN_ADMINCP')) { exit('Access Denied'); } if($operation == 'setting') { if(submitcheck('editsubmit')) { C::t('common_setting')->update('xianyu_city_list', $_POST['city_list']); C::t('common_setting')->update('xianyu_max_images', intval($_POST['max_images'])); cpmsg('设置更新成功', 'action=plugins&operation=config&identifier=xianyu&anchor=setting', 'succeed'); } $settings = array( 'city_list' => C::t('common_setting')->fetch('xianyu_city_list'), 'max_images' => C::t('common_setting')->fetch('xianyu_max_images') ); } ?> <!-- admincp模板代码略 -->

玄学参数:allowxianyu字段必须在common_usergroup_field表中存在,否则DZ权限系统不识别。若插件卸载后重装,需手动检查该字段是否存在,缺失则执行:

ALTER TABLE `pre_common_usergroup_field` ADD COLUMN `allowxianyu` TINYINT(1) NOT NULL DEFAULT '0';

5. 避坑指南:那些让站长凌晨三点还在查日志的致命细节

Discuz插件调试没有console.log,全靠error_log()和数据库日志。以下是我在三个项目中踩出的血坑,按发生频率排序:

5.1 现象:上传图片后页面白屏,Nginx日志显示PHP Fatal error: Call to undefined function image_resize()

原因:DZ X3.5的image_resize()函数依赖GD库,但部分CentOS 7服务器默认不装php-gd扩展,且错误不抛出到前端。
解决:

# Ubuntu/Debian sudo apt-get install php-gd # CentOS/RHEL sudo yum install php-gd # 重启PHP服务 sudo systemctl restart php-fpm # 验证 php -m | grep gd

5.2 现象:用户发布后跳转到404,URL形如plugin.php?id=xianyu:view&pid=123但页面空白

原因:plugin.php路由未正确解析view动作,常见于source/plugin/xianyu/xianyu.class.php中discuz_plugin_xianyu::view()方法未声明或拼写错误(如写成viwe())。
解决:

  • 检查xianyu.class.php是否包含public function view()方法
  • 方法内必须有global $_G;且调用$pid = intval($_GET['pid']);
  • 最后必须输出模板:include template('plugin/xianyu/view');

5.3 现象:同城帖子始终显示“无数据”,xianyu_get_local_posts()返回空数组

原因:DZ用户资料中的city字段为空,而插件代码$user_profile['city']取不到值,导致LIKE '%{}%'匹配所有城市失败。
解决:

  • 后台→用户→用户字段→编辑“所在城市”字段,勾选“允许用户修改”并设为必填
  • 执行SQL批量补全(示例补杭州):
UPDATE pre_common_member_profile SET city='杭州市' WHERE city='';

5.4 现象:私信发送后对方收不到通知,home.php?mod=spacecp&ac=pm里也无新消息

原因:DZ PM系统依赖common_notification表记录,但插件未触发通知。
解决:在发送私信后手动插入通知:

// 在xianyu_send_message()函数末尾 DB::insert('common_notification', array( 'uid' => $touid, 'type' => 'pm', 'new' => 1, 'authorid' => $_G['uid'], 'author' => $_G['username'], 'note' => '您收到一条新私信', 'dateline' => TIMESTAMP ));

5.5 现象:后台插件列表显示“已启用”,但plugin.php?id=xianyu:publish返回“插件不存在”

原因:插件目录名xianyu与plugin.xml中id="xianyu"不一致,或目录权限不足(PHP无法读取.php文件)。
解决:

  • 确认source/plugin/xianyu/目录下文件权限为644,目录权限为755
  • 执行chmod -R 644 source/plugin/xianyu/*.php
  • 检查plugin.xml首行<plugin id="xianyu"与目录名完全一致(区分大小写)

6. 进阶技巧:用DZ原生能力做闲鱼关键词监控与自动发货雏形

“闲鱼关键词监控”不是要你写爬虫,而是利用DZ已有的搜索日志和用户行为数据。同样,“自动发货”在DZ语境下指订单状态变更后自动发站内信——这才是可落地的最小闭环。

6.1 关键词监控:从搜索日志挖高意向词

DZ自带搜索日志表pre_searchindex,记录所有用户搜索词。闲鱼式监控只需统计高频二手词:

-- 统计近7天搜索词Top 20(排除单字和通用词) SELECT keyword, COUNT(*) as cnt FROM pre_searchindex WHERE dateline > UNIX_TIMESTAMP(NOW() - INTERVAL 7 DAY) AND LENGTH(keyword) >= 2 AND keyword NOT IN ('二手','闲置','转让','求购','卖','买') GROUP BY keyword ORDER BY cnt DESC LIMIT 20;

将结果导出为CSV,用Python做简单词云(pip install wordcloud matplotlib):

import pandas as pd from wordcloud import WordCloud import matplotlib.pyplot as plt df = pd.read_csv('xianyu_search_keywords.csv') text = ' '.join(df['keyword'].tolist()) wc = WordCloud(font_path='/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', width=800, height=400, background_color='white').generate(text) plt.figure(figsize=(10,5)) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig('xianyu_keyword_cloud.png', dpi=200, bbox_inches='tight')

落地价值:把生成的词云贴在运营后台,告诉运营同事“杭州用户最近狂搜‘MacBook Pro M1’,快上相关帖子”——这才是关键词监控的真实用途。

6.2 自动发货:用DZ定时任务触发站内信

DZ的cron系统可每5分钟扫描一次xianyu_orders表,状态为“已付款”时自动发信:

// 在source/plugin/xianyu/cron/xianyu_ship.php中 function xianyu_ship_cron() { global $_G; // 查找已付款未发货订单 $orders = DB::fetch_all("SELECT * FROM ".DB::table('xianyu_orders')." WHERE status = 2 AND ship_time = 0"); foreach ($orders as $order) { // 发送站内信(复用DZ PM系统) C::t('common_member_notification')->insert(array( 'uid' => $order['seller_uid'], 'type' => 'system', 'new' => 1, 'authorid' => 0, 'author' => '系统', 'note' => "订单 #{$order['oid']} 已付款,请尽快发货", 'dateline' => TIMESTAMP )); // 更新发货时间 DB::query("UPDATE ".DB::table('xianyu_orders')." SET ship_time = '".TIMESTAMP."' WHERE oid = '{$order['oid']}'"); } }

在后台→工具→定时任务→添加新任务:

  • 名称:闲鱼订单发货提醒
  • 脚本文件:source/plugin/xianyu/cron/xianyu_ship.php
  • 执行周期:每5分钟

后悔药:DZ定时任务失败不会报错,只在data/log/cron_*.log中记录。务必定期检查该目录,发现xianyu_ship.php无日志即说明未执行——常见原因是cron开关未开启(后台→全局→性能优化→开启定时任务)。

我坚持不用任何第三方框架改DZ,是因为见过太多项目:初期炫技用Vue重写前端,半年后DZ升级导致整个二手模块崩掉,只能回滚。而用原生插件,哪怕DZ升到X4,只要保持plugin.xml和钩子接口不变,核心逻辑照常运行。这套方案不是银弹,但它让我在三个不同行业的社区站里,把二手交易从“凑合能用”做到“用户主动分享链接拉新”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询