简介:一套直接面向同城生活服务领域的跨平台客户端源代码,兼容App、小程序和H5三端,整体思路对标58同城、赶集网等分类信息平台。平台以信息发布、置顶、刷新、佣金分成及广告位作为核心收费模式,每个大类的栏目和每类信息下的字段均可自定义,能够覆盖招聘求职、房产租售、二手交易、拼车出行、生活服务、社区论坛等多种本地化业务,适合创业者、产品经理以及负责分类信息项目的开发者用于快速验证业务或进行二次开发。资源包采用zip格式,整体大小约34.61MB,目前公开的文件类型与文件数量暂未提供,实际内容需以下载后为准。该资源已有496人浏览学习,从客户端源码角度看,其中应包含多端工程目录、接口定义、界面资源及配置文档等关键部分,可帮助开发者理解信息发布流程、自定义栏目、置顶佣金、订单支付和权限控制等完整业务闭环。对于希望低成本搭建并运营本地生活服务平台的团队或个人,这是一份颇具参考价值的完整代码样例,能有效缩短从产品构思到上线Demo的周期。
1. 跨平台生活服务源码:一套代码跑 App、小程序和 H5 到底省了什么
解开一个名为“客户端源代码跨平台app小程序h5兼容58同城赶集网生活服务分类+信息发布置顶佣金+社区论坛+房产招聘二手自定义栏目等.zip”的压缩包,里面通常是一个 uniapp 前端工程加一个后端 API 工程。所谓“跨平台”不是把三个独立项目打包在一起,而是用一套 Vue 语法,在编译期分别产出 iOS/Android 包、微信小程序和标准 H5 网页。分类信息、置顶付费、佣金分账、社区论坛、房产招聘二手这些 58、赶集已验证过的业务形态,才是这套源码真正值钱的部分;跨端只是减少客户端重复开发的手段,业务建模和表设计才是后期能不能改得动的关键。这篇文章按“工程结构—核心业务—扩展栏目—多端上线”的顺序,把每一层应该怎么接、参数怎么设、坑在哪讲清楚,适合接手这类源码做二次开发、或者打算自己从零搭生活服务平台的团队。
2. 跨平台工程怎么搭:从解压源码到三端可编译的最小结构
这一类 zip 解压后常见两层结构:前端client/(uniapp),后端server/(PHP 或 Java 居多)。先把前端在三端跑通,再谈业务扩展,顺序不能反。
2.1 为什么生活服务类项目默认选 uniapp
跨平台方案有 React Native、Flutter、Taro、uni-app 几类,但“App + 小程序 + H5 三端同产出”这个诉求下,uniapp 的编译管线覆盖最直接。Taro 偏重小程序和 H5,App 输出要借 React Native;Flutter 不产小程序。uniapp 把小程序、App(通过离线 SDK 或云打包壳)、H5 三条产物都收进同一个build流程,而且它编译到小程序/H5 是源码级转换,调试时可以直接在微信开发者工具和浏览器里断点,不需要额外起原生项目。
这种取舍对“分类 + 发布 + 论坛”这类强模板页面很合适:列表页、详情页、发布表单高度重复,一套组件拿到三端渲染,差别集中在支付、定位、分享这些偶发的平台能力调用上。
2.2 最小可编译工程:创建命令和目录约定
如果 zip 里只给后端没有前端工程,可以用官方模板重新初始化一套,再把业务页面迁进去:
npx degit dcloudio/uni-preset-vue#vite life-services cd life-services npm install npm run dev:h5 npm run build:mp-weixin三条命令的产物分别落到dist/dev/h5、dist/build/mp-weixin。App 端不直接产出原生工程,需要在 HBuilderX 里“发行—原生App云打包”,或者接离线 SDK 自行打壳,这是源码里最容易漏掉的一步:很多压缩包只给了前端源码,App 壳要自己准备。
工程目录里几个关键位置的职责如下:
| 路径 | 作用 | 二次开发时改动频率 |
|---|---|---|
| pages/ | 页面路由,uni-app 约定pages.json里注册 | 高 |
| components/ | 跨页面复用组件,列表卡片、筛选栏、发布表单 | 高 |
| utils/ | 请求封装、鉴权、公共函数 | 中 |
| static/ | 图标、启动图、tabbar 图片(小程序端必须本地文件) | 中 |
| api/ | 接口定义与参数组装 | 高 |
pages.json里globalStyle和tabBar字段的差异点要尽早确认:小程序的 tabBar 图标必须本地图片,H5 端可以用字体图标;App 端navigationStyle定制自由度最高,但custom模式下导航栏完全自理,返回按钮、标题都要自己渲染,团队没经验时不建议第一版就开。
2.3 条件编译:跨端代码的隔离与合并
三端共用一套业务代码,不等于所有代码行都三端跑。“同一份逻辑,各端要不同实现”的场景要靠条件编译:
// utils/location.js export function getCurrentLocation() { // #ifdef H5 return fetchLocationFromBrowser(); // 浏览器 geolocation 或 JS-SDK // #endif // #ifdef MP-WEIXIN return uni.getLocation({ type: 'wgs84' }); // 小程序会弹授权框 // #endif // #ifdef APP-PLUS return plus.geolocation.getCurrentPosition(); // App 壳原生能力 // #endif }条件编译里的平台宏注意两点:H5只对浏览器环境生效,微信公众号内嵌页同样走这段;MP-WEIXIN只代表微信小程序,如果后面要跑支付宝小程序,得另加MP-ALIPAY分支。平台判断不能在运行期用process.env之类的前端环境变量蒙混,会同时打进多余代码,某些小程序平台直接报编译错误。
3. 分类、发布、置顶、佣金:生活服务信息流的四张核心表
生活服务类平台的第一闭环是“看分类 → 发布信息 → 付钱置顶 → 平台分佣金”。源码质量高低,先看这四张表设计,而不是看页面数量。
3.1 分类数据模型:无限级分类与栏目自定义的底座
分类表是所有栏目的入口。一套可用十年的结构至少要含父级、路径、层级、排序四个字段:
CREATE TABLE category ( cat_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, pid INT UNSIGNED NOT NULL DEFAULT 0, cat_name VARCHAR(50) NOT NULL, level TINYINT NOT NULL DEFAULT 1, -- 1一级 2二级 3三级 cat_path VARCHAR(255) NOT NULL DEFAULT '', -- 例如 "1,12,120" icon_url VARCHAR(255) DEFAULT '', extend_schema TEXT, -- 自定义栏目字段 JSON auto_publish TINYINT NOT NULL DEFAULT 1, -- 是否免审直接上架 sort_order INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;cat_path存从根到自身的 ID 链,查询某个二级分类下所有三级分类时,直接WHERE cat_path LIKE '1,12,%',避免递归。level和path存在冗余,但读取性能换写入复杂度,在分类这种低频变更表上是划算的。auto_publish字段决定了该分类下的信息要不要过审,房产中介和二手手机这种高风险类目建议关掉免审。
3.2 信息发布接口:状态机、置顶订单一次生成
信息表通常不细分房产表、招聘表,而是共用一张主表加扩展字段。主表关注发布人、分类、标题、正文、图片、状态和置顶到期时间:
CREATE TABLE info ( info_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, cat_id INT UNSIGNED NOT NULL, uid INT UNSIGNED NOT NULL, title VARCHAR(80) NOT NULL, content TEXT, images TEXT, -- JSON 数组 extra_json TEXT, -- 自定义栏目字段值 status TINYINT NOT NULL DEFAULT 0, -- 0待审 1展示 2下架 3删除 is_top TINYINT NOT NULL DEFAULT 0, top_expire DATETIME DEFAULT NULL, -- 置顶到期时间 reply_count INT NOT NULL DEFAULT 0, view_count INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;发布接口的常见做法是:校验分类和字段 → 判断该分类是否免审 → 入库 → 如果选了置顶,先生成待支付置顶订单。置顶不是发布动作的一部分,而是独立的付费动作,代码上要把两者拆开:
// POST /api/info/publish async function publish(req, res) { const { cat_id, title, content, images = [], top_days = 0, extra_json = {} } = req.body; const uid = req.auth.uid; const cat = await db.find('category', cat_id); if (!cat || cat.status !== 1) { return res.json({ code: 400, msg: '分类不存在或已下线' }); } const info_id = await db.insert('info', { cat_id, uid, title, content, images: JSON.stringify(images.slice(0, 9)), // 限制图片数量防止脏数据 extra_json: JSON.stringify(extra_json), status: cat.auto_publish ? 1 : 0, is_top: 0, created_at: new Date() }); let order_no = null; if (top_days > 0) { const fee = computeTopFee(cat_id, top_days); // 分类不同、单价不同 order_no = genOrderNo(uid); await db.insert('pay_order', { order_no, uid, biz_type: 'top', biz_id: info_id, amount: fee, status: 0, // 0未支付 1已支付 2退款 created_at: new Date() }); } res.json({ code: 0, info_id, order_no }); }参数和字段含义对照如下:
| 参数 | 用途 | 注意 |
|---|---|---|
| cat_id | 决定展示在哪一级栏目、走哪套扩展字段 | 前台只能选level=3的叶子分类 |
| top_days | 置顶天数,按天计费 | 与computeTopFee联动,注意设置上限,避免跨年订单 |
| images | 图片 JSON 数组 | 上传服务返回 URL,先落库再回填,防止前端还没传完就提交 |
| auto_publish | 分类的免审开关 | 审核流里status=0的信息不进搜索索引 |
| biz_type | 区分top、refresh、vip等付费类型 | 佣金结算按这个字段分账 |
流程上有个细节:置顶费用不要在发布时直接扣余额(会出现未支付但信息已置顶的脏状态),而是先生成未支付订单,用户从“我的发布”里看到待支付再付款,支付回调里才真正置顶并开始计算到期时间。
3.3 置顶排序:别用 order by top_expire desc
新手最容易写成ORDER BY top_expire DESC,结果就是:过期置顶信息的top_expire是过去时间,负权重会把它直接挤到列表底,但新发的普通信息也被压在它下面;更糟的是置顶到期时间是乱的,权重只依赖到期时间点,先到期的置顶反而排前面。标准做法是把“是否在置顶期”当成一档,再加权重细分:
SELECT info_id, title FROM info WHERE cat_id = :catId AND status = 1 ORDER BY (top_expire > NOW()) DESC, -- 置顶期内排第一档 top_weight DESC, -- 同档内权重,刷新置顶时加 created_at DESC LIMIT 20;top_weight是浮点数,置顶续费时、刷新信息时都要递增。后台加一个每分钟跑一次的定时任务,把top_expire < NOW()的记录is_top置 0,同时top_weight降为 0,避免过期信息无限期占据一档的尾巴。
3.4 佣金分账:平台、渠道、发布者三方比例
佣金在这套系统里至少有三类:置顶费佣金(发布者付钱买流量,平台和渠道分)、信息服务佣金(发起咨询或成交后抽成)、会员佣金(包月/包年会员的渠道分成)。无论哪种,分账字段要统一落在支付订单里。
// Payments 回调后触发分账 async function onPaySuccess({ order_no, paid_amount }) { const order = await db.find('pay_order', { order_no }); await db.update('pay_order', { status: 1, paid_at: new Date() }, order_no); const split = { platform_rate: 0.10, // 平台技术费 agent_rate: 0.05, // 区域代理/渠道 publisher_rate: 0.85 // 可提现余额,冻结 T+1 }; await db.insert('commission_log', { order_no, biz_type: order.biz_type, amounts: JSON.stringify({ platform: round2(paid_amount * split.platform_rate), agent: round2(paid_amount * split.agent_rate), publisher: round2(paid_amount * split.publisher_rate) }), status: 0 }); // 发布者余额加账但先冻结,避免刚付款就提现跑路 await db.exec( 'UPDATE user_balance SET frozen = frozen + ? WHERE uid = ?', [split.publisher_rate * paid_amount, order.uid] ); }三方比例不要硬编码,后台做成配置项,不同分类可以不同。特别提示:佣金入账和地图这类外接 SDK 没关系,难点在退款:置顶刚付款就下架内容,要能在commission_log里反向冲正,设计分账表时留status(冻结/可提/已提现/冲正)四态,退款只改状态不删流水,对账时才能把账对上。
4. 社区论坛 + 房产招聘二手 + 自定义栏目:如何在共用表上长出新栏目
论坛、房产、招聘、二手在数据形态上差异很大,但“信息流”本质相同:标题 + 正文 + 发布人 + 时间 + 扩展字段。好的源码取舍是把不同栏目都放在 category 和信息表组合之上,差异只体现在扩展字段。
4.1 论坛板块结构:帖子表和回帖计数冗余
论坛的最小闭环是版块、帖子、回复三张表。版块复用 category 表,cat_type字段标forum;帖子复用 info 表,固定cat_id指向版块下专属分类。回复表单独建,因为回帖信息流独立于帖子本身:
CREATE TABLE forum_reply ( reply_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, info_id INT UNSIGNED NOT NULL, uid INT UNSIGNED NOT NULL, content TEXT NOT NULL, reply_pid INT UNSIGNED NOT NULL DEFAULT 0, -- 楼中楼父回复 created_at DATETIME NOT NULL, KEY idx_info (info_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;热帖排序常用ORDER BY reply_count DESC配时间窗口,reply_count在回复插入时用一个UPDATE info SET reply_count = reply_count + 1同步冗余,比COUNT(*)子查询省很多,回帖删除时反向减一。楼中楼用reply_pid实现,注意查询时只做两层嵌套,超过两层 UI 体验会很差。
4.2 房产招聘二手:一套公共字段还是三套独立模型
58、赶集下每个类目都有专属筛选:房产要有面积、户型、朝向、楼层;招聘要有学历、经验、薪资区间;二手要新旧成色、原价、转让价。如果每个栏目建一张独立表,栏目一多库表爆炸,且新增栏目必须动数据库迁移。
稳妥的方案是:info 表存公共字段,extra_json存栏目差异字段,MySQL 里的 JSON 字段在 8.0 可以直接按JSON_EXTRACT(extra_json, '$.area')查询,性能能接受。下面是一个房产分类的扩展字段定义:
| 字段 | 类型 | 筛选场景 | 典型值 |
|---|---|---|---|
| area | number | 面积区间筛选 | 89.5(单位㎡) |
| house_type | string | 户型卡片展示 | 三室两厅 |
| orientation | string | 朝向筛选 | 南北 |
| floor | string | 楼层 | 高层/中层/低层或具体层数 |
| total_price | number | 总价区间 | 268(单位万) |
| unit_price | number | 列表头图展示 | 30112(单位元/㎡) |
extra_json里没有的栏目再增量追加,不用重建表。这套做法的边界是:字段值无法建索引关联查询,一旦需要“按面积区间 + 总价区间组合搜索”,数据量大到单表扛不住时,只能把extra_json抽列到扩展表或者同步进 Elasticsearch,这也是后期最常见的架构升级路径。
4.3 自定义栏目的动态表单:一份 JSON 驱动发布页
“自定义栏目”功能拆开看就是八个字:动态表单、动态列表。后台在category.extend_schema里配置字段,前端发布页按 schema 渲染控件,列表页按 schema 渲染筛选器。
// 后台配置的 schema,存 category.extend_schema [ { "field": "area", "label": "面积", "type": "number", "placeholder": "单位:㎡", "required": true }, { "field": "house_type", "label": "户型", "type": "select", "options": ["一室", "两室", "三室", "四室以上"] }, { "field": "orientation", "label": "朝向", "type": "select", "options": ["南北", "南", "东", "西", "北"] }, { "field": "floor", "label": "楼层", "type": "text" } ]uniapp 里用一个转发控件component :is不够直观,常见做法是写一个dynamic-form.vue,把控件类型映射成 uni-app 内置组件:
<template> <view v-for="f in schema" :key="f.field" class="form-item"> <text class="label">{{ f.required ? '*' : '' }}{{ f.label }}</text> <picker v-if="f.type === 'select'" :range="f.options" @change="onSelectChange(f, $event)"> <view class="picker-value">{{ form[f.field] || '请选择' }}</view> </picker> <input v-else :type="f.type === 'number' ? 'digit' : 'text'" v-model="form[f.field]" :placeholder="f.placeholder" /> </view> </template> <script setup> const props = defineProps(['schema', 'modelValue']); const form = reactive({ ...props.modelValue }); function onSelectChange(f, e) { form[f.field] = f.options[e.detail.value]; } </script>提交时后端不要直接信任extra_json,必须按category.extend_schema里的类型逐项校验:number项要Number.isFinite,select项要落在 options 内,required项空值直接 400。列表页的筛选器同样由 schema 生成,筛选项只对type: 'select'和type: 'number'生成对应组件,这样新增栏目只需配 JSON,前端发布页实际上永远只维护一份。
4.4 内容审核:小程序强制安全检测不能省
小程序的体验版、审核版和正式版,消息和内容类目都会被平台例行抽查。信息发布接口里至少要接一层安全过滤:
- 文本:调微信
security.msgSecCheck(小程序端必须,App/H5 可以走自建词库) - 图片:调
security.imgSecCheck,发布时同步检测,不要异步补扫(用户上传的图随时可能下架,异步会漏) - 自建词库:维护一个敏感词表,
content.replace命中即标记待审
注意接口调用顺序:先同步过词库(内存级别),再调微信安全接口(网络级别),最后落库。只调微信接口不做本地拦截,会出现高频词接口限流后发布接口超时的现象。
5. 多端打包与上线验证:App、小程序、H5 最容易翻车的四个点
前几章把业务打通之后,最后一道关卡是把同一个源码分别交付给三个平台。三类产物差异比较大,下面四个点踩过一遍基本就齐了。
5.1 定位权限:三端三套配置,一个页面三份代码
上面条件编译里已经写了定位的获取逻辑,真正的坑在配置层:
| 端 | 配置位置 | 关键动作 |
|---|---|---|
| H5 / 微信公众号 | 域名 HTTPS + JS-SDK 签名 | 正式环境必须配jsapi_ticket签名,本地 devtools 里需要勾选不校验合法域名 |
| 微信小程序 | manifest.json的 mp-weixin 权限声明 | 登录微信公众平台配置requiredPrivateInfos里的getLocation |
| App | manifest 的 App 模块配置 + 隐私政策弹窗 | 国产安卓应用商店要求声明“获取位置”目的,不弹窗可能被拒 |
uni.getLocation在 App 端如果没在 manifest 里勾选定位模块,返回的fail信息是“未配置定位权限”,看起来像接口报错,实际上是打包配置少了模块。
5.2 支付、分享与 H5 内嵌页的差异
H5 和 App 都能调起微信支付或支付宝支付,但三端的支付初始化参数不同:小程序支付必须拿openid生成预支付单,H5 支付走JSAPI拉起的支付中间页,而 App 支付用的是统一下单里的app接口,三个trade_type不一样,后端支付路由要按客户端端识别分发。
分享海报也有版本差异:H5 用 canvas 导出,小程序用canvas 2d接口,App 端则可能要用html2canvas再走原生分享。这部分代码不要写在同一段逻辑里,拆成utils/share/{h5,mp-weixin,app}.js三个文件用条件编译引入,后期维护成本最低。
5.3 发布后的真机验证顺序
打好包后按这个顺序过一轮:
- 微信开发者工具里跑
build:mp-weixin产物,检查页面路由、tabBar 图标、sitemap 配置 - 真机预览小程序,重点测定位授权、图片选择、支付拉起
- H5 产物放到已配好域名的服务器上,微信内打开一次,确认 JS-SDK 签名有效期和分享描述正常
- App 云打包后装测试机,检查
plus能力是否生效,特别是相册、位置、推送 - 最后把分类、发布、置顶、论坛、自定义栏目按“手机上完整发一条二手信息”的路径走一遍
每次改动都要重新输出三端产物,H5 的产物目录是dist/build/h5,小程序是dist/build/mp-weixin,两个目录独立,发布时别传错目录。
一个值得专门盯的细节是:小程序端pages.json里的navigationStyle设为custom后,H5 端会默认保留浏览器导航栏,两边的返回逻辑不同,最好在onLoad里按端判断是否隐藏uni.navigateBack的入口。跨端验证清单里把这一条放在支付之前先看,导航出问题比支付出问题更让测试人员困惑。
本文还有配套的精品资源,点击获取