uni-app x · UTS · 插件市场 · CPS 聚合
做返利/导购类应用的同学应该都有同感:想覆盖美团外卖、饿了么、到店团购、酒店、电影、打车这些本地生活场景,就得逐个去对接各家 CPS 平台。每家平台的接口地址不一样、鉴权方式不一样、字段命名不一样、返回结构更是五花八门。等项目跑起来,光维护这些 SDK 就够喝一壶的。
于是我封装了一个 uni-app x 的 UTS 插件 ls-cps:一套统一的 API,背后用适配器模式隔离各 CPS 平台的差异,目前内置 聚推客 平台适配,跑通了活动列表、转链、订单全链路,并且已经用 HBuilderX 的 Android 编译 + 真实接口联调验证通过。今天把这篇文章写下来,既是分享,也是记录过程中踩的几个坑。
为什么用 UTS 插件
- 一套代码,全端编译:UTS 是 uni-app x 的强类型语言,可同时编译到 Android / iOS / 小程序(微信、支付宝等)/ H5,不需要为每个端各写一套。
- 类型安全:相比 JS 的 Any 满天飞,UTS 在编译期就能抓出字段拼写错误,重构成本低。
- 插件市场分发:打成
uni_modules插件后,用户复制目录即可使用,接入成本极低。
插件能力一览
| 方法 | 说明 |
|---|---|
LocalLife.init(config) | 初始化并选择 CPS 平台 |
LocalLife.getHotList(categoryId, page, pageSize) | 获取推广活动列表 |
LocalLife.getCategoryTree() | 获取类目树 |
LocalLife.createLink(productId, linkType) | 生成推广链接(H5 / 小程序码等) |
LocalLife.getOrderList(status, page, pageSize) | 查询订单与佣金 |
LocalLife.search(keyword, categoryId, page, pageSize) | 商品搜索 |
LocalLife.getBalance() | 账户余额 |
说明:
search/getBalance是通用接口预留,部分 CPS 平台(如聚推客的联盟活动模式)不提供商品搜索与余额查询,此时会抛出明确的业务提示,业务层可据此优雅降级。
支持的 uni-app 版本与前端平台兼容性
uni-app 版本要求
- 插件基于 uni-app x(UTS)开发。uni-app x 项目中使用 UTS 调用 UTS 插件自 HBuilderX 3.9 起支持,正式环境建议使用 HBuilderX 4.0 及以上版本(推荐最新稳定版,当前已在 HBuilderX 5.24 上编译验证通过)。
- 请注意:uni-app x 与经典 uni-app(vue)项目引擎不同。本插件为 UTS 插件,主要面向 uni-app x 项目开发与验证;经典 uni-app(vue2/vue3)项目理论上可通过 UTS 插件机制引用(HBuilderX 3.6.8+),但本插件未在经典 uni-app 项目做完整验证,如需使用请先自测。
前端平台兼容性
| 前端平台 | 支持情况 | 说明 |
|---|---|---|
| App-Android | ✅ 已验证 | HBuilderX 编译 + 真机联调通过 |
| App-iOS | ✅ 声明支持 | 仅依赖 uni.request 与标准全局 API,建议真机自测 |
| H5 | ✅ 声明支持 | 注意浏览器跨域(CORS),生产建议服务端中转 |
| 微信小程序 mp-weixin | ✅ 声明支持 | 需配置 request 合法域名,建议 https |
| 支付宝小程序 mp-alipay | ✅ 声明支持 | 同上 |
| 百度小程序 mp-baidu | ✅ 声明支持 | 同上 |
| 抖音小程序 mp-toutiao | ✅ 声明支持 | 同上 |
package.json 的
uni_modules.platforms已声明以上全部平台,插件本身不包含任何平台私有 API,因此具备跨端编译能力;未逐一真机验证的平台,建议按上表做一次冒烟测试。
兼容性注意事项
- 插件只依赖
uni.request与标准全局对象(Date/Map/Promise/encodeURIComponent),这些能力在所有目标端均可用,不需要额外安装依赖。 - 网络域名:聚推客官方接口为
http://api.jutuike.com(明文 http),需要注意:- App-Android:Android 7.0(API 24)起系统默认只信任系统证书、限制明文 http(Network Security Configuration),若真机联调请求失败,请确认明文流量配置;App-Android 最低支持 Android 5.0(API 21)。
- 小程序端:必须在对应平台后台把接口域名加入 request 合法域名白名单,且微信/支付宝普遍要求 https,建议接入 https 中转服务。
- H5 端:存在浏览器跨域(CORS)问题,生产环境建议走服务端中转。
- 想省心的话,最稳妥的用法是把请求都转发到自己的服务端(顺便解决密钥不落地的问题),插件里把
baseUrl指向你的中转网关即可。
快速上手
把 uni_modules/ls-cps 目录复制进你的 uni-app x 项目,然后:
import { LocalLife } from '@/uni_modules/ls-cps'
// 1. 初始化(以聚推客为例)
LocalLife.init({
platform: 'jutuihe',
apiKey: '你的聚推客apikey', // 单密钥鉴权
sid: '你的跟单标识', // 生成推广链接时必传的自定义跟单参数
baseUrl: '' // 留空使用插件内置地址
})
// 2. 获取类目(外卖 / 团购 / 打车 / 酒店 / 电影 ...)
const cats = await LocalLife.getCategoryTree()
console.log('分类数:', cats.data.length)
// 3. 拉取某分类下的推广活动
const hot = await LocalLife.getHotList('美团', 1, 20)
const act = hot.data.list[0]
console.log('活动:', act.id, act.title)
// 4. 生成推广链接(返回 H5 短链 / 长链 / 小程序路径 / 小程序码)
const link = await LocalLife.createLink(act.id, 'h5')
console.log('H5 短链:', link.data.url)
console.log('小程序:', link.data.extra.get('we_app_id'))
// 5. 查询订单与佣金(插件自动按平台规则补全时间窗口)
const orders = await LocalLife.getOrderList('', 1, 20)
console.log('订单数:', orders.data.total)
所有方法统一返回 Promise<CpsResult<T>>,业务层只需要认一套返回协议:
interface CpsResult<T> {
code: number // 0 表示成功
msg: string // 提示信息
data: T | null // 业务数据
}
统一数据模型
不管底层是哪家平台,上层拿到的都是统一模型,列表页、详情页、订单页的代码可以完全复用:
CpsProduct:id / title / price / commission / image / platform / extraCpsLink:url / shortUrl / posterImage / expireTime / extraCpsOrder:orderId / productTitle / status / statusText / commission / extraCpsPageResult<T>:list / page / pageSize / total / hasMore
各平台特有字段统一放进 extra: Map<string, any>,既不丢失信息,也不污染通用模型。
架构设计:适配器模式隔离平台差异
┌────────────┐ ┌──────────────────────────────┐
│ LocalLife │ │ ProviderFactory │
│ (门面) │──▶│ register('jutuihe', ...) │
└────────────┘ └──────────────┬───────────────┘
▼
┌───────────────────────────┐
│ ICpsProvider (接口) │
│ search / getHotList / │
│ createLink / getOrder... │
└────────────┬──────────────┘
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ JutuiheProvider │ │ XxxProvider │
│ (聚推客) │ │ (其他平台,预留) │
└─────────────────┘ └─────────────────┘
index.uts:对外门面LocalLife,只暴露统一 API。providers/ICpsProvider.uts:适配器接口 +ProviderConfig,定义统一契约。providers/jutuihe/:聚推客具体实现。JutuiheProvider.uts:协议层,负责参数组装、请求、字段映射。jutuihe.config.uts:纯配置——接口地址、请求方式、成功码、分页字段、字段映射全部收敛在这里。平台接口变了,改配置即可,不动业务代码。
common/:http 封装、JSON 工具、签名器(md5)等基础设施。
想接入新平台?三步搞定:
- 新建
providers/<platform>/目录,实现ICpsProvider全部方法; - 把该平台的接口地址 / 字段映射写进自己的 config;
- 在
LocalLife.init()里ProviderFactory.register('平台名', () => new XxxProvider()),然后platform传对应名字即可切换。
业务层代码一行不用改。
实战踩坑记录
联调真实接口时踩了几个很有意思的坑,都很有代表性,记录一下:
坑 1:CPS 平台的"单密钥"鉴权,没有签名
市面上很多电商 CPS 是 appKey + appSecret 双密钥 + MD5 签名。聚推客这类本地生活联盟却只有一把 apikey,不需要签名。如果硬套"双密钥 + 签名"模型,永远鉴权不过。
所以 ProviderConfig 设计成双模式兼容:保留 appKey/appSecret/pid(双密钥平台用),新增可选的 apiKey/sid/pubId(单密钥平台用)。聚推客实现里直接取 apiKey 作为 apikey 参数,跳过签名逻辑。
坑 2:uni-app x 的 uni.request,GET 请求别用 Map 当 data
App 端第一次跑,getHotList 直接返回 平台返回错误: 0 非法访问。但同样的 apikey 我用 curl 直连 http://api.jutuike.com/union/act_list?apikey=... 却正常返回数据。
原因:uni-app x 中 uni.request 对 GET 请求并不会把 Map<string,string> 类型的 data 正确拼进 URL query,服务端收不到 apikey 就判定"非法访问"。
修复方案:手动把参数 encodeURIComponent 后拼进 URL,再发起请求:
let url = base + apiPath
let first = true
for (const k of params.keys()) {
url += (first ? '?' : '&')
first = false
url += k + '=' + encodeURIComponent(params.get(k)!)
}
坑 3:订单接口的 code:0 不一定是错误
聚推客订单接口,没有订单数据时返回的是 {"code":0, "msg":"暂无相应的订单数据。", "data":[]}——这在语义上其实是一次"成功但空结果"。
如果沿用统一的 code === successCode 判断,就会把"没订单"误判成平台报错。
解决:为订单解析增加 allowEmpty 模式——只要返回了 data(哪怕是空数组),就视为正常结果,不再抛异常:
private extractData(raw: any, allowEmpty = false): any {
const result = this.toResult(raw)
if (!this.isSuccess(result.code)) {
if (!(allowEmpty && result.data != null)) {
throw new Error('平台返回错误: ' + result.code + ' ' + result.msg)
}
}
return result.data
}
坑 4:文档示例的字段位置,和真实返回不一致
聚推客转链接口文档示例里 miniCode 在顶层,但饿了么活动的真实返回里,miniCode 藏在 we_app_info 里面。文档给的"标准答案"和线上真实数据有出入。
处理方式:解析时做双层兼容——顶层取不到就从 we_app_info 里取,取不到才给空串:
const topMini = JsonUtil.getStr(raw, 'miniCode')
const innerMini = JsonUtil.getStr(weApp, 'miniCode')
const miniCode = topMini.length > 0 ? topMini : innerMini
教训:对接 CPS 平台,别全信文档,一定要用真实接口联调一遍。
结语
这个插件目前作为第一个平台的样板实现,走通了"统一门面 → 适配器 → 配置化 → 真实接口联调"的完整链路,并且已经通过 HBuilderX 的 Android 编译验证。后续扩展其他 CPS 平台(高佣联盟、好省、美团联盟等),只需要在适配器层补一个 Provider 就行,业务层完全无感。
如果你也在做本地生活 / 返利类应用,或者对 uni-app x 的 UTS 插件封装感兴趣,欢迎一起交流。