淘宝选品接口实战:从召回、详情、佣金到转链的 Java 接入笔记

1. 先厘清:"淘宝选品接口"不是单个 API

做淘宝客/导购/内容电商选品,本质是一条链路:

  1. 候选召回:关键词、类目、榜单、活动物料、高佣/大额券筛选;

  2. 详情校验:标题、主图、价格、库存、店铺分、类目、服务标签;

  3. 收益评估:佣金率、券后价、补贴、定向计划、历史销量;

  4. 转链产出:长链/短链、淘口令、二合一券链接;

  5. 效果归因:点击、付款、结算、退款、渠道/会员/Relation ID。

官方常用接口大致分三类:

  • 导购/淘宝客选品taobao.tbk.dg.material.optional 通用物料搜索,taobao.tbk.dg.material.optional.upgrade 升级版,taobao.tbk.dg.material.recommend 按物料/官方商品库召回,taobao.tbk.item.info.get 商品详情。官方文档把 material.optional 定义为"通用物料搜索API(导购)",返回结果中含 total_resultsresult_list.map_data、券信息、佣金字段、类目与店铺字段等。 升级版接口在文档中标注为免费、无需用户授权,且收益信息放在 publish_info.income_info、价格促销放在 price_promotion_infotaobao.tbk.item.info.get 则属于淘宝客公用物料信息查询。

  • 商家自用商品/库存 :如果你选品是为了自己店铺运营而不是 CPS 推广,看 taobao.items.onsale.gettaobao.item.seller.gettaobao.items.inventory.get 这类需要店铺授权的接口;开放平台把交易/商品场景接口列在商品同步、订单同步等流程中。

  • 转化组件taobao.tbk.tpwd.createtaobao.tbk.spread.gettaobao.tbk.dg.punish.order.get 之外的订单明细类接口等。核心原则:能走官方 API 就不要抓页面,不要逆向滑块,不要买黑产 Cookie。

2. 权限与凭证:选品系统的"地基"

接入流程通常是:注册开放平台/联盟账号 → 实名或企业认证 → 创建应用 → 选择类目与 API 权限组 → 提交业务场景 → 获取 app_key/app_secret → 淘宝客侧再维护推广位 adzone_id / pid。开放平台通用流程包括创建应用、获取 API 密钥、按需求申请接口权限并提交资料审核。

实践建议:

  • app_secret、联盟 pid、渠道 relation_id/special_id 只放服务端;前端、App、小程序永不直连签名。

  • 选品服务与转链服务拆库:选品库允许过期,点击转链必须实时。

  • adzone_id 做多租户隔离;不要跨推广位混用链接。

  • 关注限流与权限变更:官方文档与控制台权限页是唯一事实来源,第三方博客只用于排错参考。

3. Java 公共参数与签名示例

下面示例采用 TOP 常见 sign_method=md5 规则:剔除空值与 sign,按键排序,secret + k1v1k2v2... + secret 后 MD5 大写。若你的应用后台开通的是 HMAC 类签名,按当前文档替换 sign() 实现即可;不要硬编码旧规则

java 复制代码
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.TreeMap;

public final class TopClient {
    private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String md5TopSign(Map<String, String> params, String appSecret) {
        TreeMap<String, String> sorted = new TreeMap<>();
        params.forEach((k, v) -> {
            if (v != null && !v.isEmpty() && !"sign".equals(k)) sorted.put(k, v);
        });

        StringBuilder sb = new StringBuilder(appSecret);
        sorted.forEach((k, v) -> sb.append(k).append(v));
        sb.append(appSecret);

        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] dig = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder();
            for (byte b : dig) hex.append(String.format("%02X", b));
            return hex.toString();
        } catch (Exception e) {
            throw new IllegalStateException("sign error", e);
        }
    }

    public static String hmacSha256Hex(Map<String, String> params, String appSecret) {
        TreeMap<String, String> sorted = new TreeMap<>();
        params.forEach((k, v) -> {
            if (v != null && !v.isEmpty() && !"sign".equals(k)) sorted.put(k, v);
        });
        StringBuilder sb = new StringBuilder();
        sorted.forEach((k, v) -> sb.append(k).append(v));

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] raw = mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder();
            for (byte b : raw) hex.append(String.format("%02x", b));
            return hex.toString();
        } catch (Exception e) {
            throw new IllegalStateException("hmac sign error", e);
        }
    }

    public static Map<String, String> commonParams(String appKey, String method) {
        Map<String, String> p = new TreeMap<>();
        p.put("app_key", appKey);
        p.put("method", method);
        p.put("format", "json");
        p.put("v", "2.0");
        p.put("timestamp", LocalDateTime.now().format(TS));
        p.put("sign_method", "md5"); // 若后台配置 hmac,可切换 sign/hmacSha256Hex
        return p;
    }

    public static String form(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        params.forEach((k, v) -> {
            if (v == null) return;
            if (sb.length() > 0) sb.append('&');
            sb.append(URLEncoder.encode(k, StandardCharsets.UTF_8))
              .append('=')
              .append(URLEncoder.encode(v, StandardCharsets.UTF_8));
        });
        return sb.toString();
    }
}

4. 选品召回:升级版物料搜索

升级版返回更贴近选品:收益、近 2 小时/当日推广销量、最终促销价、未来活动价、满减路径都在结构化字段里。官方示例中 publish_info.income_info.commission_rate 为比例乘 100 后的整数,如 55 表示 5.5%;two_hour_promotion_salesdaily_promotion_sales 可用于热度粗排;price_promotion_info.final_promotion_price 更接近用户到手价判断。

java 复制代码
import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;
import okhttp3.*;

public class TbkSelection {
    private static final String GATEWAY = "https://eco.taobao.com/router/rest";
    private final OkHttpClient http = new OkHttpClient();
    private final String appKey;
    private final String appSecret;
    private final String adzoneId;

    public TbkSelection(String appKey, String appSecret, String adzoneId) {
        this.appKey = appKey;
        this.appSecret = appSecret;
        this.adzoneId = adzoneId;
    }

    public String searchOptionalUpgrade(String keyword, long pageNo, long pageSize) throws IOException {
        Map<String, String> p = new TreeMap<>(TopClient.commonParams(appKey, "taobao.tbk.dg.material.optional.upgrade"));
        p.put("adzone_id", adzoneId);
        p.put("q", keyword);
        p.put("page_no", String.valueOf(pageNo));
        p.put("page_size", String.valueOf(pageSize));
        // 选品常用过滤:按需开启,不要一次堆满
        // p.put("has_coupon", "true");
        // p.put("sort", "tk_rate_des");     // 以文档枚举为准
        // p.put("start_price", "20");
        // p.put("end_price", "200");
        // p.put("start_tk_rate", "100");    // 如文档要求乘100,则1%传100
        // p.put("is_tmall", "true");
        // p.put("itemloc", "杭州");
        p.put("sign", TopClient.md5TopSign(p, appSecret));

        Request req = new Request.Builder()
                .url(GATEWAY)
                .post(RequestBody.create(TopClient.form(p), MediaType.get("application/x-www-form-urlencoded; charset=utf-8")))
                .build();

        try (Response resp = http.newCall(req).execute()) {
            String body = resp.body() == null ? "" : resp.body().string();
            if (!resp.isSuccessful()) throw new IOException("HTTP " + resp.code() + ": " + body);
            return body;
        }
    }
}

注意:不同接口对佣金/比例的缩放可能不同。旧版字段常见 commission_rate=1550表示15.5%,新版/升级版的 income_rate 示例直接给 5.50。入库前统一归一化为 Decimal commissionRate,不要在前端混用"1550 / 15.5 / 0.155"。

5. 详情校验与商品补充信息

taobao.tbk.item.info.get 适合做候选商品批量详情补齐;但不要把详情接口当成高并发实时库存源。 CPS 场景更重要的是"能否推广、券是否有效、当前到手价多少"。

java 复制代码
public String itemInfoGet(String numIids, String fields) throws IOException {
    Map<String, String> p = new TreeMap<>(TopClient.commonParams(appKey, "taobao.tbk.item.info.get"));
    p.put("num_iids", numIids);     // 多个 id 用逗号,具体上限看文档
    p.put("fields", fields);        // 按文档指定返回字段,别 fields=*
    p.put("sign", TopClient.md5TopSign(p, appSecret));

    Request req = new Request.Builder()
            .url(GATEWAY)
            .post(RequestBody.create(TopClient.form(p), MediaType.get("application/x-www-form-urlencoded; charset=utf-8")))
            .build();
    try (Response resp = http.newCall(req).execute()) {
        String body = resp.body() == null ? "" : resp.body().string();
        if (!resp.isSuccessful()) throw new IOException("HTTP " + resp.code() + ": " + body);
        return body;
    }
}

6. 选品打分:别只按佣金率排序

建议把候选商品归一化成 SelectionCandidate 后打分:

java 复制代码
public class SelectionCandidate {
    public String itemId;
    public String title;
    public String categoryName;
    public String shopTitle;
    public BigDecimal finalPrice;       // 到手价/预估促销价
    public BigDecimal commissionRate;   // 0.155 = 15.5%
    public BigDecimal commissionAmount; // 预估佣金,若接口返回
    public Long dailySales;
    public Long twoHourSales;
    public BigDecimal shopDsr;          // 注意部分接口用*5或差值表示,先归一
    public Boolean hasCoupon;
    public String couponInfo;
    public String clickUrl;
    public String couponShareUrl;
}

public class ScoreService {
    public double score(SelectionCandidate c) {
        // 示例权重:按业务调参;所有分子分母先做异常值截断
        double income = clamp(c.commissionAmount == null ? 0 : c.commissionAmount.doubleValue(), 0, 100);
        double hot = log1p(c.dailySales == null ? 0 : c.dailySales);
        double price = c.finalPrice == null ? 0 : clamp(100 - c.finalPrice.doubleValue(), 0, 100);
        double dsr = clamp(c.shopDsr == null ? 0 : c.shopDsr.doubleValue(), 0, 5) * 20;
        double couponBoost = Boolean.TRUE.equals(c.hasCoupon) ? 8 : 0;
        return 0.45 * income + 0.25 * hot + 0.15 * price + 0.10 * dsr + couponBoost;
    }

    private static double clamp(double v, double min, double max) { return Math.max(min, Math.min(max, v)); }
    private static double log1p(double v) { return Math.log1p(Math.max(0, v)) * 10; }
}

工程上要加三类护栏:

  • 新鲜度:券/价/佣金强时效字段短缓存 1--5 分钟;点击/下单链路实时取链。

  • 黑名单:退款率高、DSR 低、类目禁投、品牌词侵权、店招与实物不符候选直接过滤。

  • 实验桶:同关键词下保留 10% 流量探索低佣金但高转化商品,避免系统只推"高佣低质"。

7. 转链与数据合规

转链建议独立服务:入参 item_id/券 me/推广位/渠道标识,出参短链或淘口令;落库保留 relation_id/special_id、时间戳与版本号,便于归因。淘口令/短链本身也是敏感资源,不要在前端日志、埋点明文里完整打印。

合规红线:

  • 不绕过滑块、不逆向 App 签名、不抓取详情页增量;

  • 不用多账号轮询突破 QPS;遇到限流做退避与队列削峰;

  • 买家隐私字段最小化保存、加密存储、按 retention 清理;

  • API 已覆盖字段不重复爬虫化采集;竞品监控用授权数据或公开聚合服务并审查条款。

8. 常见坑位清单

  • Remote service error:先重试一次并退避;频繁出现查权限、QPS、参数枚举、签名时间偏移。

  • 佣金率单位混乱:统一存 decimal,入库前做 rate > 1 ? rate/100 : rate 这类归一要谨慎,最好按接口字段单独适配器。

  • 库存为 0:CPS 选品不等于商家实时库存;不要把 stock=0 当唯一下架依据,结合详情/活动状态。

  • 券面额误导:coupon_info=满299元减20元 对低客单无效,必须计算券后价门槛。

  • 返回字段缺失:多半是 fields 未指定、权限未开通或场景不支持;测试期全字段,上线收敛字段。

  • 第三方"超级搜索/高佣申请"封装:可作灰度补充,但核心链路要有官方回退与字段校验,避免上游改字段直接打穿。

9. 上线 Checklist

  • 权限:物料搜索/详情/转链权限组已开通,adzone_id 与推广位一致;

  • 签名:服务端生成;密钥 KMS/配置中心管理;时钟 NTP 同步;

  • 重试:仅对幂等查询重试;写操作与转链防重;

  • 缓存:候选 5--15 分钟,券价 1--5 分钟,点击转链不缓存;

  • 监控:按接口/错误码/耗时/比例单位异常监控;

  • 数据:字段版本化、原始报文抽样留档、归因可回放;

  • 合规:隐私加密、授权回调 HTTPS、权限最小化、调用审计。

一句话总结:淘宝选品接口的核心不是"拿到商品列表",而是把 召回---详情---收益---转链---归因 做成可回放、可降级、可审计的系统;代码可以薄,字段归一与合规治理必须厚。

如遇任何疑问或有进一步的需求,请随时与我私信或者评论联系。

相关推荐
泡茶喝茶写代码1 小时前
量化数据进阶:多维实战篇(第 11 篇):历史涨跌停价:涨跌停序列与止损线测算
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据api
景熙55231 小时前
单体项目编写思路和落地形式
java·开发语言
Ivanqhz1 小时前
Slope One 算法详解:与矩阵分解、共现矩阵的异同
java·服务器·网络·人工智能·深度学习
Bs_MoneyMagnet2 小时前
基于springboot+vue的非遗物质文化遗产系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
万物智能2 小时前
波形发生AD9833模块配置—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
java
计算机毕设定制辅导-无忧学长2 小时前
《基于SpringBoot的未成年人健康知识科普平台的设计与实现》
java·开发语言·vue.js·spring boot·未成年人健康知识科普平台
Wang's Blog2 小时前
Java 项目实战: 外卖平台-数据库环境搭建与十一张表结构解析
java·服务器·redis
白远山2 小时前
上海24小时自助健身房系统软件开发实战指南
java·架构·uni-app·需求分析
泡海椒2 小时前
jquick-pdf 表格实战:动态数据 PDF 报表生成
java·开发语言·pdf