☰
Discuz精仿闲鱼二手交易插件开发实战
2026/10/8 1:15:40 网站建设 项目流程

简介:本资源是一套专为Discuz!站长与PHP开发者设计的二手交易功能扩展插件,精准复刻闲鱼式商品发布体验,解决传统论坛缺乏轻量级C2C交易模块的痛点,适用于希望提升用户活跃度与社区粘性的中小型Discuz站点。压缩包共34个文件,含19个核心PHP业务逻辑文件、4个XML插件配置与语言包、4个HTML前端模板页、3个GIF界面元素及字体、CSS与图片等配套资源,整体仅62KB,结构精简、部署轻量。已有777人学习下载,表明其在实战场景中具备较高参考价值。读者可直接获取完整可运行的插件源码体系,涵盖后台管理入口、商品发布/展示/搜索全流程、多编码语言包(GBK/UTF8/BIG5)、AJAX交互逻辑、安全防护基础(防SQL注入)、以及安装/卸载/升级全生命周期脚本,便于快速集成、二次开发或逆向学习Discuz插件架构。

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

你手头有个跑着Discuz! X3.4的老论坛,用户活跃但转化低;想加二手交易功能,又不想推倒重来、重写整套用户体系和权限逻辑。这时候,“Discuz精仿闲鱼二手发布”就不是一句营销话术——它是一套在DZ原生架构上复刻闲鱼核心交互链路的工程方案:从发布页的多图上传+自定义属性弹窗,到列表页的卡片式瀑布流+价格/距离/新旧度三排序维度,再到详情页的“我想要”按钮+私聊入口+信用标识,全部用DZ插件机制实现,不改核心文件、不引入Vue/React前端框架、不依赖外部API网关。它解决的不是“有没有二手功能”,而是“如何让老论坛用户零学习成本地完成从浏览→询价→成交的闭环”。适合中小社区运营者、本地生活类站点站长、教育机构内部资源流转平台搭建者——你不需要懂Laravel或SpringBoot,但得会看DZ的source/plugin/目录结构、能改模板变量、敢动common.inc.php里的钩子调用顺序。


2. 拆解“精仿”二字:为什么必须绕开DZ默认主题,用独立插件+定制模板重构交易流

2.1 “精仿”的本质是业务流重定向,而非UI像素级复制

Discuz默认的“分类信息”模块(如portal或forum下的分类版块)本质是单帖模型:一个标题+一段描述+一张图+几个自定义字段。而闲鱼的核心是商品实体模型:主图轮播、规格参数表、卖家信用快照、动态库存状态、议价历史折叠区。强行在DZ原有帖子里塞这些字段,会导致数据库冗余(每个二手帖要额外建10+个custom_字段)、模板逻辑爆炸(viewthread.htm里嵌套5层if判断)、后台管理混乱(管理员要在“帖子管理”和“分类信息管理”两个入口来回切换)。所以“精仿”的第一步,是用DZ插件机制注册全新入口路由,把/plugin.php?id=ershou:publish作为发布起点,所有数据存入独立表pre_plugin_ershou_goods,彻底隔离于pre_forum_post。

2.2 插件结构设计:三层分离,确保可维护性

我们采用标准DZ插件三层结构,但关键路径做了强化:

  • install.php:不只是建表,还注入全局钩子。例如在global_footer钩子里插入JS检测当前页面是否为商品详情页,动态加载地图SDK(用于显示“距您X公里”);
  • admincp.php:提供独立后台,含商品审核队列(带“一键通过/驳回/下架”快捷操作)、敏感词过滤开关(针对二手交易高频违规词如“代充”“外挂”)、发货状态机配置(待发货→已发货→已签收→退款中);
  • template/目录:不复用DZ默认模板,而是新建ershou_list.htm(瀑布流)、ershou_detail.htm(详情页)、ershou_publish.htm(发布页),每个模板只加载必需CSS/JS,避免与论坛主站样式冲突。

提示:DZ插件模板无法直接使用{eval}执行PHP逻辑,所有动态数据必须通过插件的discuz_application::instance()->init()阶段预加载到$GLOBALS['ershou_data'],再在模板中用{$_G['ershou_data']['xxx']}调用。这是新手最容易卡住的点——别试图在.htm里写<?php echo ... ?>,DZ会直接报错。

2.3 数据库设计:用“扩展表+状态机”替代DZ原生字段硬编码

pre_plugin_ershou_goods表结构精简但覆盖全链路:

字段名类型说明示例值
gidint(10) PK AI商品ID12873
uidint(10)发布者UID(关联pre_common_member)4521
titlevarchar(100)商品标题(带防刷限长)iPhone 13 Pro 256G 银色 99新
pricedecimal(10,2)标价(支持小数,单位元)4299.00
statustinyint(1)状态机:0草稿/1待审核/2已上架/3已售罄/4已下架/5已删除2
locationvarchar(50)城市+区域(结构化存储,非纯文本)上海:浦东新区:张江
picstextJSON数组存储图片URL(兼容DZ附件系统)["http://xxx/1.jpg","http://xxx/2.jpg"]
attrstext序列化属性键值对(如“成色”:“99新”,“购买渠道”:“官网”)a:2:{s:6:"成色";s:6:"99新";s:12:"购买渠道";s:6:"官网";}

这个设计规避了DZ原生customfield的三大缺陷:字段不可搜索、无法建立联合索引、修改字段需全量重建缓存。比如按“成色=95新且价格<3000”筛选,直接WHERE attrs LIKE '%95新%' AND price < 3000即可,无需触发DZ的customfield慢查询。


3. 发布页实现:如何用DZ原生组件搭出闲鱼式多步骤表单

3.1 多图上传:复用DZ附件系统,但改造上传逻辑

Discuz自带的swfupload组件只支持单图,而闲鱼要求至少3张图(主图+细节图+实拍图)。解决方案是拦截DZ的ajax.php?action=swfupload请求,在服务端做多图合并处理:

// source/plugin/ershou/ajax.php if($_GET['action'] == 'swfupload') { $uid = $_G['uid']; $file = $_FILES['Filedata']; // 1. 验证文件类型和大小(限制JPG/PNG,单图≤5MB) if(!in_array(strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)), ['jpg','jpeg','png'])) { exit('{"error":"仅支持JPG/PNG格式"}'); } if($file['size'] > 5242880) { exit('{"error":"单图不能超过5MB"}'); } // 2. 调用DZ原生附件上传函数,但指定自定义目录 $attach = C::t('forum_attachment_n')->insert(array( 'uid' => $uid, 'dateline' => TIMESTAMP, 'filename' => $file['name'], 'filesize' => $file['size'], 'attachment' => 'ershou/'.date('Ym').'/'.random(8).'.jpg', // 自定义路径 'isimage' => 1, 'width' => 0, 'height' => 0, 'thumb' => 0, 'remote' => 0 ), true); // 3. 返回JSON格式URL,供前端JS拼接 $url = getforumimg($attach['aid'], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,......

逻辑说明:

  • C::t('forum_attachment_n')是DZ 3.4+的附件表操作类,insert()返回新记录ID(aid);
  • getforumimg($aid, 1, ...)生成带防盗链参数的图片URL,1表示启用缩略图;
  • 前端JS收到URL后,存入数组window.ershou_pics = [],点击“发布”时作为JSON字符串提交给插件主逻辑。

3.2 自定义属性弹窗:用DZ的dialog组件实现动态字段组

闲鱼发布页的“手机成色”“是否带票”“配件齐全度”等字段,不是固定列表,而是根据商品类目动态加载。我们复用DZ后台的dialog弹窗机制:

// 在 ershou_publish.htm 中 function showAttrDialog() { var dialog = $.dialog({ title: '填写商品属性', content: '<div id="attr-content"></div>', width: 600, height: 400, ok: function() { // 收集所有input值,序列化为JSON var attrs = {}; $('#attr-content input').each(function(){ attrs[$(this).data('key')] = $(this).val(); }); $('#attrs-json').val(JSON.stringify(attrs)); // 存入隐藏域 } }); // 异步加载对应类目的属性模板 $.get('plugin.php?id=ershou:attr&cid='+$('#category').val(), function(html){ $('#attr-content').html(html); }); }

后端source/plugin/ershou/function/attr.php根据cid(类目ID)返回HTML片段,例如手机类目返回:

<div class="attr-item"> <label>成色:</label> <select name="chengse">// source/plugin/ershou/list.php $lastid = intval($_GET['lastid']); $limit = min(20, intval($_GET['limit'])); // 最大20条,防恶意limit=10000 $where = "status=2"; // 只查已上架 if($_GET['city']) $where .= " AND location LIKE '{$_GET['city']}%'"; if($_GET['price_min']) $where .= " AND price >= {$_GET['price_min']}"; if($lastid) { $where .= " AND gid < {$lastid}"; // 注意是 <,不是 <= } $query = DB::query("SELECT * FROM ".DB::table('plugin_ershou_goods')." WHERE {$where} ORDER BY gid DESC LIMIT {$limit}"); $goods = array(); while($row = DB::fetch($query)) { $goods[] = $row; } // 返回JSON,供前端追加DOM echo json_encode(array( 'list' => $goods, 'next_lastid' => !empty($goods) ? end($goods)['gid'] : 0, 'has_more' => count($goods) == $limit ));

逻辑说明:

  • gid < {$lastid}确保结果严格递减,避免ID重复或跳过;
  • next_lastid是本页最后一条的gid,前端下次请求时填入lastid;
  • has_more布尔值控制是否显示“加载更多”按钮。

4.2 详情页性能优化:用DZ的memory缓存+静态化预生成

闲鱼详情页需实时显示“距您X公里”(依赖用户GPS)、“卖家在线状态”(需查pre_common_session),但大部分内容(标题、图片、描述)是静态的。我们采用混合缓存策略:

  • 静态部分:商品基础信息(title,pics,attrs)存入memory缓存,Key为ershou_detail_{$gid},TTL=3600秒;
  • 动态部分:用户位置、在线状态、最新询价数,在模板中用AJAX单独请求plugin.php?id=ershou:detail_dynamic&gid=12873;
  • 防爬补充:对/plugin.php?id=ershou:detail&gid=xxx路径增加Referer白名单校验(只允许来自本站域名),并在Nginx层配置limit_req zone=ershou_detail burst=5 nodelay。

注意:DZ的memory缓存依赖config/config_memory.php配置,若未启用Memcached或Redis,会自动降级为文件缓存(data/cache/目录),此时需确保data/可写且磁盘充足。


5. 避坑指南:在DZ里做闲鱼仿站,这5个坑让我重装了3次环境

5.1 现象:发布成功后商品不显示在列表页,后台审核队列也为空

原因:插件安装时未正确执行install.php中的数据库建表语句,或pre_plugin_ershou_goods表的ENGINE被设为MyISAM(DZ 3.4+默认用InnoDB)。MyISAM不支持事务,当发布流程中某步失败(如图片上传超时),会导致商品记录插入但附件未关联,状态机卡在“待审核”,而审核队列查询条件WHERE status=1却因表引擎问题无法命中。
解决:登录phpMyAdmin,选中pre_plugin_ershou_goods表 → “操作” → “存储引擎” → 改为InnoDB;再运行source/plugin/ershou/install.php中的建表SQL手动补全。

5.2 现象:多图上传后,详情页只显示第一张图,其余图URL为空

原因:DZ附件系统在swfupload回调中,对attachment字段的路径拼接有长度限制(默认255字符),而自定义路径ershou/202405/xxxxxx.jpg加上域名后可能超长,导致C::t('forum_attachment_n')->insert()返回空aid。
解决:修改source/class/table/table_forum_attachment_n.php第120行左右,将attachment字段类型从varchar(255)改为text,并同步更新数据库表结构。

5.3 现象:按“距离排序”时,列表页报错Unknown column 'distance' in 'order clause'

原因:“距您X公里”是前端JS计算的伪字段,不能直接用于SQLORDER BY。很多开发者误在list.php里写ORDER BY distance ASC,而distance根本不存在于数据库。
解决:距离排序必须由前端实现——先用ORDER BY gid DESC拉取20条,再用JS遍历每条记录的location字段(如“上海:浦东新区:张江”),调用百度地图API的getDistance()计算直线距离,最后用Array.sort()重排DOM顺序。

5.4 现象:后台审核通过后,商品状态变为2(已上架),但列表页仍不显示

原因:DZ的discuz_application::instance()->init()阶段,插件钩子global_footer被其他插件(如SEO优化插件)提前终止,导致商品状态变更后的缓存刷新逻辑未执行。
解决:在source/plugin/ershou/index.php顶部添加define('IN_DISCUZ', true);,并在common.inc.php末尾显式调用require_once DISCUZ_ROOT.'./source/plugin/ershou/cache_refresh.php';,绕过钩子依赖。

5.5 现象:手机端访问详情页,图片轮播失效,控制台报Uncaught ReferenceError: swiper is not defined

原因:DZ移动端模板(template/default/touch/)未引入Swiper JS,而插件模板ershou_detail.htm直接写了new Swiper(...)。PC端正常是因为template/default/common/header.htm已加载Swiper,但Touch模板走的是另一套资源链。
解决:在ershou_detail.htm顶部添加条件判断:

<!--{if $_G['mobile']}--> <script src="{STATICURL}js/swiper.min.js"></script> <!--{else}--> <script src="{STATICURL}js/swiper-bundle.min.js"></script> <!--{/if}-->

并确保static/js/目录下存放了适配移动端的精简版Swiper。


6. 进阶技巧:用DZ的cron任务+微信模板消息,把二手交易做成闭环运营

6.1 每日自动清理超期草稿:防止数据库垃圾堆积

Discuz的定时任务(cron)是处理后台无人值守任务的黄金方案。我们创建source/plugin/ershou/cron/clean_drafts.php:

<?php // 删除7天前的草稿(status=0) DB::delete('plugin_ershou_goods', 'status=0 AND dateline < '.(TIMESTAMP - 604800)); // 同时清理对应附件(需关联pre_forum_attachment表) $draft_gids = DB::fetch_all("SELECT gid FROM ".DB::table('plugin_ershou_goods')." WHERE status=0 AND dateline < ".(TIMESTAMP - 604800)); foreach($draft_gids as $g) { $attach_ids = DB::fetch_all("SELECT aid FROM ".DB::table('forum_attachment')." WHERE tid={$g['gid']} AND pid=0"); foreach($attach_ids as $a) { C::t('forum_attachment')->delete($a['aid']); // 调用DZ原生删除函数,自动清理文件 } } ?>

在DZ后台 → 管理中心 → 工具 → 定时任务 → 添加新任务:

  • 名称:清理二手草稿
  • 脚本文件:plugin/ershou/cron/clean_drafts.php
  • 执行周期:每天凌晨2点(0 2 * * *)

提示:DZ的cron任务依赖服务器crontab,若你的虚拟主机不支持,可改用“伪定时”——在global_footer钩子里检查$_G['timestamp'] % 86400 < 300(每天前5分钟执行),但精度较低。

6.2 微信模板消息通知:让“有人想要您的商品”真正触达用户

闲鱼的核心体验是“即时反馈”。我们利用DZ的uc_api(UCenter)打通微信服务号:

模板ID触发场景关键参数
AT001商品被“我想要”thing1: 商品标题,number2: 价格,phrase3: 卖家昵称
AT002商品已售罄thing1: 商品标题,date2: 下架时间

实现逻辑:当用户点击“我想要”按钮,前端提交到plugin.php?id=ershou:want&gid=12873,后端除了插入pre_plugin_ershou_want记录,还调用:

// 获取卖家的微信OpenID(需提前在用户中心绑定) $seller = C::t('common_member')->fetch($_G['uid']); if($seller['wx_openid']) { $template_data = array( 'first' => '您发布的商品有人感兴趣啦!', 'keyword1' => array('value' => $goods['title'], 'color' => '#173177'), 'keyword2' => array('value' => '¥'.$goods['price'], 'color' => '#173177'), 'keyword3' => array('value' => $buyer['username'], 'color' => '#173177'), 'remark' => '快去私聊了解详情吧~' ); // 调用微信模板消息API(需提前配置AppID/AppSecret) $url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=".$access_token; $res = dfsockopen($url, 0, json_encode(array( 'touser' => $seller['wx_openid'], 'template_id' => 'AT001', 'data' => $template_data ))); }

参数说明:

  • dfsockopen()是DZ封装的cURL函数,比原生curl_exec()更兼容老版本PHP;
  • access_token需通过https://api.weixin.qq.com/cgi-bin/token接口获取,建议缓存到data/cache/wx_access_token.php;
  • 模板消息需在微信公众平台后台申请,且用户必须关注服务号并完成绑定。

6.3 一个血泪经验:永远在install.php里写回滚脚本

上线前我自信满满地删掉了uninstall.php,结果测试时发现商品属性弹窗的CSS冲突了论坛首页。紧急修复需要:

  1. 手动删除pre_plugin_ershou_goods表;
  2. 清空pre_common_plugin中ershou相关记录;
  3. 删除source/plugin/ershou/整个目录;
  4. 清除data/cache/下所有cache_ershou_*文件。

现在我的每个插件都带uninstall.php,开头就是:

// source/plugin/ershou/uninstall.php DB::query("DROP TABLE IF EXISTS ".DB::table('plugin_ershou_goods')); DB::query("DELETE FROM ".DB::table('common_plugin')." WHERE identifier='ershou'"); // 删除模板缓存 @unlink(DISCUZ_ROOT.'./data/cache/cache_ershou_*'); // 删除附件目录(谨慎!) // rmdir(DISCUZ_ROOT.'./data/attachment/ershou/');

——哪怕只是开发阶段,也要把卸载当成必经流程。毕竟,线上环境没有后悔药,只有备份和回滚。

希望帮到你。

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

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

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

立即咨询