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

admin6小时前淘宝api9

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、权限最小化、调用审计。
一句话总结:淘宝选品接口的核心不是“拿到商品列表”,而是把 召回—详情—收益—转链—归因 做成可回放、可降级、可审计的系统;代码可以薄,字段归一与合规治理必须厚。


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

相关文章

从客户需求到 API 落地:淘宝商品详情批量爬取与接口封装实践

一、需求分析:不只是"爬数据"那么简单在电商数据分析场景中,客户的核心需求通常包含三个层次:基础层:批量获取商品标题、价格、销量、评价等公开信息加工层:数据清洗、格式统一、实时更新...

1688 关键词搜索选品实战指南:从海量货源中精准锁定爆款

在 1688 这个拥有超过 60 万家工厂的批发平台上,"找得到"比"找得多"更重要。每天上新数以万计的商品,如果没有一套系统化的关键词搜索选品方法论,你只会陷...

第三方爬虫获取淘宝商品详情数据的 API 接口实践指南

一、背景与需求在电商数据分析、价格监控、选品工具等场景中,获取淘宝商品详情数据(如标题、价格、库存、SKU、主图、详情图、销量等)是核心前提。虽然淘宝开放平台提供了官方 API(如 taobao.it...

用“爬虫”思路做淘宝 API 接口测试:从申请 Key 到 Python 自动化脚本

关键词:淘宝开放平台、API 测试、接口签名、Python 爬虫、数据驱动测试一、背景与合规说明淘宝在 2024 年升级了“反爬+合规”双策略:网页端 cookie 加密粒度更细,直接破解易触发 22...

利用 Java 爬虫获取淘宝商品详情 API 接口数据

在电商领域,淘宝作为国内领先的电商平台,拥有海量的商品数据。对于开发者和数据分析师来说,获取淘宝商品详情数据对于市场分析、价格监控、用户体验优化等场景具有重要意义。本文将详细介绍如何使用 Java 编...

淘宝海外商品详情接口实战指南:从全球开放平台到跨境铺货的全链路方案

在跨境电商和海外代购的业务场景中,"让海外用户看到淘宝商品"是第一步,也是最关键的一步。淘宝的海外商品详情接口并非单一接口,而是分散在淘宝全球开放平台(Taobao Global)...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。