• 开发教程与代码示例
  • 入门
  • 小程序框架
  • 小程序运行时
  • 自定义组件
  • 基础教程
  • 能力教程
  • 行业能力
  • 配置行业 SDK 的权限
  • 电商
  • 插件扩展能力
  • 本地团购类小程序(步骤一:前端开发)
  • 本地团购类小程序(步骤二:商品库接入)
  • 流量入口
  • 通用能力
  • 推广变现
  • 经营能力
  • AI/AR 能力
  • 性能优化
  • 安全
  • 本地团购类小程序(步骤二:商品库接入)

    收藏
    我的收藏

    前置准备

    本地团购类小程序(步骤一:前端开发)中,我们为你展示了用抖音小程序原生开发框架构造一个通用的本地团购类小程序的示例。
    但在组件中的 goods-id 字段并没有使用真实的值:
    <pay-button id="pay-button" class="pay-button" tt:if="{{isLogin}}" mode="{{2}}" goods-id="1234" goods-type="{{2}}" bind:error="handleError" bind:pay="handlePay" bind:getgoodsinfo="getGoodsInfo" bind:placeorder="userLogin" ></pay-button>
    这在实际应用中,是没有办法下单的。
    如果你想要完整走通下单流程,还需要将商品信息通过 openAPI 上传到抖音开放平台,获取到一个真实的goods-id,放到中。
    本文将指导如何将你售卖的商品相关信息上传到抖音,创建这个商品,最终得到这个 goods-id
    正式进入本教程前,请注意:
      本教程面向后端开发人员,以下所有代码示例均为服务端代码。
      本文中代码实例使用 JAVA 语言编写,版本为1.8,JAR 包使用情况如下:
    <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.3.RELEASE</version> </dependency>
      本文因展示需要,类采用静态内部类,方法采用静态方法,开发者可根据内部规范灵活拆解。
        lombok 是一个在编译时动态的帮助我们生成 POJO 类所需的 GET/SET 方法、构造器等内容的工具,常用的注解如下:
      @Data:注解在类上,将类提供的所有属性都添加 get、set 方法,并添加、equals、canEquals、hashCode、toString 方法。
      @Setter:注解在类上,为所有属性添加 set 方法、注解在属性上为该属性提供 set 方法。
      @Getter:注解在类上,为所有的属性添加 get 方法、注解在属性上为该属性提供 get 方法。
      fastjson 是一个常用的 json 处理的 SDK。

    流程简介

    整个过程的完整链路分为 5 步:
      1.获取access-token
      2.查询商品模板
      3.创建商品
      4.商品审核结果通知
      5.同步商品库存

    接口调用凭证

    接口调用凭证有效时间为 2 个小时,重复调用会导致上一次获取的凭证生效。
    获取接口调用凭证接口有 QPS 限制,为避免因频繁调用被系统限流,开发者需要通过 Redis、MySQL 等组件做好缓存工作,缓存有效时间应小于凭证有效时间。
    package com.douyin.demo.access_token; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.Collections; @Slf4j public class AccessToken { public static String url = "https://open.douyin.com/oauth/client_token/"; public static String appId = "填写自己的appid"; public static String appSecret = "填写自己的appSecret"; @lombok.Data public static class Request { @JSONField(name = "client_key") private String clientKey; @JSONField(name = "client_secret") private String clientSecret; @JSONField(name = "grant_type") private String grantType = "client_credential"; } @lombok.Data public static class Response { private Data data; } @lombok.Data public static class Data { private String accessToken; private int expiresIn; private int errorCode; private String description; private String message; } public static String GetAccessToken() throws Exception { log.info("这里需要增加从缓存中获取数据"); RestTemplate restTemplate = new RestTemplate(Collections.singletonList(new FastJsonHttpMessageConverter())); // http header HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // http body Request request = new Request(); request.setClientKey(appId); request.setClientSecret(appSecret); HttpEntity<Request> entity = new HttpEntity<>(request, headers); // 发送请求 ResponseEntity<Response> responseEntity = restTemplate.exchange(url, HttpMethod.POST, entity, Response.class); HttpStatus httpStatus = responseEntity.getStatusCode(); log.info("抖音开放平台请求 httpCode:{}", httpStatus.toString()); if (httpStatus != HttpStatus.OK) { throw new Exception("http code:" + httpStatus); } Response body = responseEntity.getBody(); log.info("抖音开放平台返回报文:{}", JSON.toJSONString(body)); if (body == null || body.getData() == null) { throw new Exception("业务异常,返回data为空"); } if (body.getData().getErrorCode() != 0) { throw new Exception("业务异常:" + body.getData().getDescription()); } String accessToken = body.getData().getAccessToken(); log.info("这里需要写入缓存"); return accessToken; } }

    查询商品模版

    查询对应商品类型的模版属性,根据模版属性创建商品,商品类型和类目 ID 参考商品库接入文档。
    package com.douyin.demo.product; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import com.douyin.demo.access_token.AccessToken; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.util.Collections; import java.util.List; @Slf4j public class ProductTemplate { public static String url = "https://open.douyin.com/goodlife/v1/goods/template/get/"; @lombok.Data public static class Request { @JSONField(name = "product_type") private String productType; @JSONField(name = "category_id") private String categoryId; } @lombok.Data public static class Response { private Data data; private Extra extra; } @lombok.Data public static class Data { private int errorCode; private String description; private List<Attr> productAttrs; private List<Attr> spuAttrs; private List<Attr> skuAttrs; private List<Attr> calendarAttrs; } @lombok.Data public static class Attr { private String desc; private boolean isMulti; private boolean isRequired; private String key; private String name; private String valueType; private String valueDemo; } @lombok.Data public static class Extra { private int errorCode; private String description; private int subErrorCode; private String subDescription; private int now; private String logid; } public static Response GetProductTemplate() throws Exception { String token = AccessToken.GetAccessToken(); RestTemplate restTemplate = new RestTemplate(Collections.singletonList(new FastJsonHttpMessageConverter())); // http header HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("access-token", token); HttpEntity<Request> entity = new HttpEntity<>(null, headers); // 组装get请求的params Request request = new Request(); request.setProductType("1"); request.setCategoryId("1001001"); // 发送请求 ResponseEntity<Response> responseEntity = restTemplate.exchange(composeGetUrl(url, request), HttpMethod.GET, entity, Response.class); HttpStatus httpStatus = responseEntity.getStatusCode(); log.info("抖音开放平台请求 httpCode:{}", httpStatus.toString()); if (httpStatus != HttpStatus.OK) { throw new Exception("http code:" + httpStatus); } Response body = responseEntity.getBody(); log.info("抖音开放平台返回报文:{}", JSON.toJSONString(body)); if (body == null || body.getData() == null) { throw new Exception("业务异常,返回data为空"); } if (body.getData().getErrorCode() != 0) { throw new Exception("业务异常:" + body.getData().getDescription()); } return body; } public static URI composeGetUrl(String url, Object params) { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(params)); jsonObject.forEach((k, v) -> map.set(k, v.toString())); return builder.queryParams(map).build().encode().toUri(); } }

    创建商品

      1.获取来客账户 ID。
      2.获取 poiid。
      3.编写创建商品接口代码,创建商品的属性值填写格式请参考商品库接入文档,请注意所有的属性值均需要转成string类型。
    package com.douyin.demo.product; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.PropertyNamingStrategy; import com.alibaba.fastjson.serializer.SerializeConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import com.douyin.demo.access_token.AccessToken; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j public class ProductCreate { public static String url = "https://open.douyin.com/goodlife/v1/goods/product/save/"; private String accountId; @lombok.Data public static class Request { private Product product; private Sku sku; // 来客账户ID private String accountId; } @lombok.Data public static class Product { private String productId; private String outId; private String productName; private String categoryFullName; private int categoryId; // 商品类型:1 : 团购套餐 3 : 预售券 4 : 日历房 5 : 门票 7 : 旅行跟拍 8 : 一日游 11 : 代金券 15:次卡 private int productType; // 业务线 1-闭环自研开发者 5-小程序 private int bizLine = 5; private String accountName; //售卖开始时间(团购商品必填),单位为s private long soldStartTime; //售卖结束时间(团购商品必填),单位为s private long soldEndTime; // 第三方跳转链接,小程序商品必填,用于货架跳商详页 // 商品类型为小程序时,out_url 格式为 json,需要包含三个字段: //- app_id: 小程序的 app_id //- path: 小程序服务页面路径 //- params: 上面 path 需要使用到的服务参数 private String outUrl; private List<Poi> pois; private Map<String, String> attrKeyValueMap; private ProductExt productExt; } @lombok.Data public static class Poi { private String poiId; private String supplierExtId; } @lombok.Data public static class ProductExt { // "true - 审核通过自动上架 /false - 审核通过不自动上架" private boolean autoOnline; // 测试商品字段 private TestExtra testExtra; } @lombok.Data public static class TestExtra { private List<String> uids; //标记商品是否为测试商品,当test_flag=true时, 1、uids数量需要大于0 2、小程序商品必须传trade_url 3、库存数不能大于50 若要取消测试标记:须指定flag=false private boolean testFlag; } @lombok.Data public static class Sku { private String skuName; //"原价,团购创建时如有commodity属性可不填,会根据菜品搭配计算原价,单位分, 计算方式: 菜品搭配x选n,菜品组价格从大到小排序,累加n个菜品组价格得出原价" private int originalAmount; private int actualAmount; private Stock stock; private String outSkuId; //状态 1-在线 ; 默认传1 private int status = 1; private Map<String, String> attrKeyValueMap; } @lombok.Data public static class Stock { //库存上限类型,为2时stock_qty和avail_qty字段无意义 1-有限库存 2-无限库存 private int limitType; // 总库存,limit_type=2时无意义 private int stockQty; } @lombok.Data public static class Response { private Data data; private Extra extra; } @lombok.Data public static class Data { private int errorCode; private String description; private String productId; } @lombok.Data public static class Extra { private int errorCode; private String description; private int subErrorCode; private String subDescription; private long now; private String logid; } public static Response saveProduct() throws Exception { String token = AccessToken.GetAccessToken(); // 此处可以通过注解注入 RestTemplate restTemplate = new RestTemplate(Collections.singletonList(new FastJsonHttpMessageConverter())); // http header HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("access-token", token); // http body Request request = composeRequest(null); SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; JSONObject snakeRequest = JSON.parseObject(JSON.toJSONString(request, config)); HttpEntity<JSONObject> entity = new HttpEntity<>(snakeRequest, headers); // 发送请求 ResponseEntity<Response> responseEntity = restTemplate.exchange(url, HttpMethod.POST, entity, Response.class); HttpStatus httpStatus = responseEntity.getStatusCode(); log.info("抖音开放平台请求 httpCode:{}", httpStatus.toString()); if (httpStatus != HttpStatus.OK) { throw new Exception("http code:" + httpStatus); } Response body = responseEntity.getBody(); log.info("抖音开放平台返回报文:{}", JSON.toJSONString(body)); if (body == null || body.getData() == null) { throw new Exception("业务异常,返回data为空"); } if (body.getData().getErrorCode() != 0) { throw new Exception("业务异常:" + body.getData().getDescription()); } return body; } private static Request composeRequest(String productId) { log.info("开发者根据自己的业务诉求创建/更新商品,并生成request"); Request request = new Request(); // 填写自己的来客账户ID request.setAccountId("7197702364353824827"); // 配置SKU信息 Sku sku = new Sku(); sku.setSkuName("开放平台团购测试SKU"); sku.setActualAmount(996); sku.setStatus(1); Stock stock = new Stock(); stock.setStockQty(100); stock.setLimitType(1); sku.setStock(stock); sku.setOriginalAmount(1399); // 配置sku属性 Map<String,String> skuAttrMap = new HashMap<>(); // 券码生成方式 "1-抖音码 2-三方码 3-预导码", //抖音码 :即交易后,抖音发券码,通过抖音侧进行核销,然后同步到开发者。当前仅针对白名单开发者开放。 //三方码 :即交易后,开发者发券码,在开发者侧进行核销,然后核销以及订单状态,同步到抖音。 //预导码:可忽略 skuAttrMap.put("code_source_type", "1"); // 菜品搭配 skuAttrMap.put("commodity", "[{\"group_name\":\"开平惊喜套餐\",\"total_count\":1,\"option_count\":1,\"item_list\":[{\"name\":\"可乐\",\"price\":1998,\"count\":1,\"unit\":\"份\"}]}]"); // 最多购买份数 skuAttrMap.put("limit_rule", "{\"is_limit\":true,\"total_buy_num\":9}"); // 市场价,即菜品搭配里的总价 skuAttrMap.put("market_price", "900"); // 收款方式 " ""1-总店结算 2-分店结算"", //总店结算:即商品的结算资金统一结算到商家(不是开发者)的收款账户。 //分店结算:按核销POI将资金结算到对应的POI的收款账户,如果POI没有设置收款账户,会将对应的POI的结算资金打款到总店账户;" skuAttrMap.put("settle_type", "1"); // 使用方式 "1-到店核销",默认值 skuAttrMap.put("use_type", "1"); sku.setAttrKeyValueMap(skuAttrMap); request.setSku(sku); // 配置Product信息 Product product = new Product(); // productId不为空,则为更新商品请求 product.setProductId(productId); product.setProductName("开放平台团购测试商品"); product.setProductType(1); product.setAccountName("开放平台团购测试商家"); product.setOutUrl("{\"params\":\"{\\\"param1\\\":\\\"xxxxx\\\",\\\"param2\\\":\\\"xxxxxx\\\"}\",\"path\":\"pages/any/path\",\"app_id\":\"xxxxx\"}"); // 配置商品属性 Map<String,String> productAttrMap = new HashMap<>(); // 预约信息 消费提示,做展示用 productAttrMap.put("appointment","{\"need_appointment\":true, \"ahead_time_type\":2, \"ahead_hour_num\":5,\"external_link\":\"urlxxx\", \"order_appointment_time_url\":\"urlxxx\"}"); // 是否开启自动延期 productAttrMap.put("auto_renew","true"); // 是否可以外带 消费提示,做展示用 productAttrMap.put("bring_out_meal","false"); // 不可使用日期 productAttrMap.put("can_no_use_date","{\"enable\": true,\"days_of_week\": [7, 1, 2, 3, 4],\"holidays\": [1, 2, 3, 4, 5],\"date_list\": [\"2022-03-08\", \"2022-03-09\"],\"holiday_dates\": {\"1\": \"2022.01.01-2022.01.03\"}}"); // 留资规则 productAttrMap.put("customer_reserved_info","{\"allow\":false}"); // 其他说明信息 productAttrMap.put("description_rich_text","[{\"note_type\":1,\"content\":\"其他说明信息-美食团购\"}]"); // 长图 productAttrMap.put("detail_image_list","[{\"url\":\"http://static.runoob.com/images/demo/demo1.jpg\"}]"); // 菜品图 productAttrMap.put("dishes_image_list","[{\"url\":\"http://static.runoob.com/images/demo/demo1.jpg\"}]"); // 环境图 productAttrMap.put("environment_image_list","[{\"url\":\"http://static.runoob.com/images/demo/demo1.jpg\"}]"); // 是否可以打包 消费提示,做展示用 productAttrMap.put("free_pack","true"); // 前台品类标签tag, 枚举 productAttrMap.put("FrontCategoryTag","[\"美食套餐\"]"); // 套餐图 productAttrMap.put("image_list","[{\"url\":\"http://static.runoob.com/images/demo/demo1.jpg\"}]"); // 商品行业类型 productAttrMap.put("IndustryType","其他"); // 是否立即确认 productAttrMap.put("IsConfirmImme","true"); // 使用规则 productAttrMap.put("Notification","[{\"title\":\"标题\",\"content\":\"内容美食1.1\"}]"); // 是否可以使用包间 productAttrMap.put("private_room","true"); // 实名信息 productAttrMap.put("real_name_info","{\"enable\":false,\"scene\":0}"); // 推荐语 productAttrMap.put("RecommendWord","推荐语"); // 建议使用人数 productAttrMap.put("rec_person_num","99"); // 最多使用人数 productAttrMap.put("rec_person_num_max","999"); // 退款政策 productAttrMap.put("RefundPolicy","2"); // 退款是否需要商家审核 productAttrMap.put("refund_need_merchant_confirm","true"); // 商品投放渠道 "1-不限制 2-仅直播间可见", productAttrMap.put("show_channel","2"); // 排序权重 productAttrMap.put("SortWeight","0"); // 是否可以享受店内其他优惠 productAttrMap.put("superimposed_discounts","true"); // 标签列表 productAttrMap.put("TagList","标签列表-待填写"); // 可使用日期 productAttrMap.put("use_date","{\"use_date_type\":1,\"use_start_date\":\"2023-05-06\",\"use_end_date\":\"2023-12-03\"}"); //可使用时间 productAttrMap.put("use_time","{\"use_time_type\":1}"); // 入口类型 1:H5 2:小程序 3:抖音 4:lynx productAttrMap.put("EntryType","2"); // 小程序提单页跳转,小程序必填,需要包含三个字段: //- app_id: 小程序的 app_id //- path: 小程序服务页面路径 //- params: 上面 path 需要使用到的服务参数 productAttrMap.put("trade_url","{\"params\":\"{\\\"param1\\\":\\\"xxxxx\\\",\\\"param2\\\":\\\"xxxxxx\\\"}\",\"path\":\"pages/any/path\",\"app_id\":\"xxxxx\"}"); product.setAttrKeyValueMap(productAttrMap); product.setBizLine(5); product.setCategoryId(1001001); product.setOutId("douyin_open_out_id_1003"); product.setSoldStartTime(1687921921); product.setSoldEndTime(1687921921); Poi poi = new Poi(); poi.setPoiId("7198096317707487269"); product.setPois(Collections.singletonList(poi)); ProductExt productExt = new ProductExt(); productExt.setAutoOnline(false); TestExtra testExtra = new TestExtra(); // 如果需要用测试商品,testFlag设置为true,并把可以看到这个测试商品的抖音账号uid填到uids字段中。 testExtra.setTestFlag(false); productExt.setTestExtra(testExtra); product.setProductExt(productExt); request.setProduct(product); return request; } }

    商品审核结果通知

      1.设置回调地址点击右上角控制台,选择需要设置回调地址的小程序。
        选择行业模版菜单,点击更改配置。
        选择消息订阅栏目中的商品,点击修改,设置自己的回调地址URL;点击实现说明可查看回调消息的格式。
      2.签名验签在小程序后台,点击开发配置,获取平台公钥。
        编写签名验签代码:
    package com.douyin.demo.util; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class RSAUtil { public static String platformPublicKey = "填写小程序的平台公钥,如何获取平台公钥可参考上一步的图示"; public static boolean verify(String httpBody, String publicKey, String signStr, String timestamp, String nonce) throws Exception { StringBuffer buffer = new StringBuffer(); buffer.append(timestamp).append("\n"); buffer.append(nonce).append("\n"); buffer.append(httpBody).append("\n"); String message = buffer.toString(); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initVerify(string2PublicKey(publicKey)); sign.update(message.getBytes(StandardCharsets.UTF_8)); return sign.verify(Base64.getDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8))); } public static PublicKey string2PublicKey(String publicKey) throws Exception{ byte[] decoded = Base64.getDecoder().decode(publicKey); return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); } }
      3.接收审核回调消息并处理。
    注意
    本代码使用了spring框架,切记类需要被spring扫描加入容器,开发者可根据自身诉求,配置messageConverter,实现自动化的http请求的body转对象操作,注意下划线和驼峰的差异。特别说明:验签对报文的顺序有要求,所以验签时必须使用原生的http body作为入参。
    package com.douyin.demo.product; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.PropertyNamingStrategy; import com.alibaba.fastjson.serializer.SerializeConfig; import com.douyin.demo.util.RSAUtil; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @RestController @Slf4j public class ProductMessage { @Data public static class Request { private String productId; // 商家审核通过 - MERCHANT_CONFIRM_SUCCESS;商家审核拒绝/超时 - MERCHANT_CONFIRM_FAIL;平台审核通过 - PASS;平台审核拒绝 - FAIL private String status; private String reason; private String operateType; } @Data public static class Response { private int errNo; private String errTips; } // 填写你在行业模版中设置的URI @PostMapping("/xx/xx") public String handleAuditMessage(@RequestBody String requestStr) throws Exception { Response response = new Response(); // 签名验签 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest servletRequest = requestAttributes.getRequest(); String logid = servletRequest.getHeader("Byte-LogId"); log.info("抖音开放平台logid:{}", logid); String noneStr = servletRequest.getHeader("Byte-Nonce-Str"); String timeStamp = servletRequest.getHeader("Byte-Timestamp"); String signature = servletRequest.getHeader("Byte-Signature"); boolean verify = RSAUtil.verify(requestStr, RSAUtil.platformPublicKey, signature, timeStamp, noneStr); if (!verify) { return composeResponse(99, "验签失败"); } // 处理请求报文 Request request = JSON.parseObject(requestStr, Request.class); // 分别处理不同的情况 if (request.status == "MERCHANT_CONFIRM_SUCCESS") { } if (request.status == "MERCHANT_CONFIRM_FAIL") { } if (request.status == "PASS") { } if (request.status == "FAIL") { } return composeResponse(0, "success"); } public static String composeResponse(int errCode, String message) { Response response = new Response(); response.setErrNo(errCode); response.setErrTips(message); SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; return JSON.toJSONString(response, config); } }

    同步商品库存

    只有上线的商品支持同步商品库存,同步商品库存不需要审核。
    package com.douyin.demo.product; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.PropertyNamingStrategy; import com.alibaba.fastjson.serializer.SerializeConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import com.douyin.demo.access_token.AccessToken; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.web.client.RestTemplate; import java.util.Collections; @Slf4j public class ProductStock { public static String url = "https://open.douyin.com/goodlife/v1/goods/stock/sync/"; @lombok.Data public static class Request { private String productId; private String outId; private Stock stock; private String accountId; } @lombok.Data public static class Stock { //库存上限类型,为2时stock_qty和avail_qty字段无意义 1-有限库存 2-无限库存 private int limitType; // 总库存,limit_type=2时无意义 private int stockQty; } @lombok.Data public static class Response { private Data data; private Extra extra; } @lombok.Data public static class Data { private int errorCode; private String description; } @lombok.Data public static class Extra { private int errorCode; private String description; private int subErrorCode; private String subDescription; private long now; private String logid; } public static Response updateStock() throws Exception { String token = AccessToken.GetAccessToken(); // 此处可以通过注解注入 RestTemplate restTemplate = new RestTemplate(Collections.singletonList(new FastJsonHttpMessageConverter())); // http header HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("access-token", token); // http body Request request = new Request(); request.setProductId("7250345348789585958"); request.setAccountId("7197702364353824827"); Stock stock = new Stock(); stock.setLimitType(1); stock.setStockQty(200); request.setStock(stock); SerializeConfig config = new SerializeConfig(); config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; JSONObject snakeRequest = JSON.parseObject(JSON.toJSONString(request, config)); HttpEntity<JSONObject> entity = new HttpEntity<>(snakeRequest, headers); // 发送请求 ResponseEntity<Response> responseEntity = restTemplate.exchange(url, HttpMethod.POST, entity, Response.class); HttpStatus httpStatus = responseEntity.getStatusCode(); log.info("抖音开放平台请求 httpCode:{}", httpStatus.toString()); if (httpStatus != HttpStatus.OK) { throw new Exception("http code:" + httpStatus); } Response body = responseEntity.getBody(); log.info("抖音开放平台返回报文:{}", JSON.toJSONString(body)); if (body == null || body.getData() == null) { throw new Exception("业务异常,返回data为空"); } if (body.getData().getErrorCode() != 0) { throw new Exception("业务异常:" + body.getData().getDescription()); } return body; } }

    后续流程

    至此,你已完成功获取到了 goods-id,只需要把它和商品的详情信息一起封装好接口,让小程序前端来获取即可。
    但一个完整的交易流程,还包含“下单”、“核销”、“退款”、“分账”等流程。在下一篇 Codelab 文档中,我们将带领你走向最后一步,同时也是最复杂的一步:接入“交易系统”。