package com.doumee.api.web;
|
|
import com.alibaba.fastjson.JSON;
|
import com.doumee.core.annotation.LoginRequired;
|
import com.doumee.core.annotation.pr.PreventRepeat;
|
import com.doumee.core.constants.Constants;
|
import com.doumee.core.constants.ResponseStatus;
|
import com.doumee.core.douyin.DouyinClient;
|
import com.doumee.core.douyin.dto.DouyinBaseResp;
|
import com.doumee.core.douyin.dto.DouyinPrepareParam;
|
import com.doumee.core.douyin.dto.DouyinPrepareResp;
|
import com.doumee.core.douyin.dto.DouyinShopPoiResp;
|
import com.doumee.core.douyin.dto.DouyinVerifyParam;
|
import com.doumee.core.exception.BusinessException;
|
import com.doumee.core.model.ApiResponse;
|
import com.doumee.core.model.PageData;
|
import com.doumee.core.model.PageWrap;
|
import com.doumee.core.utils.ID;
|
import com.doumee.dao.business.model.DouyinVerifyLog;
|
import com.doumee.dao.business.model.DouyinVerifyRecord;
|
import com.doumee.service.business.DouyinProductService;
|
import com.doumee.service.business.DouyinVerifyLogService;
|
import com.doumee.service.business.DouyinVerifyService;
|
import io.swagger.annotations.Api;
|
import io.swagger.annotations.ApiOperation;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RestController;
|
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.List;
|
import java.util.stream.Collectors;
|
|
/**
|
* 抖音(小程序端:团购验券 + 联调测试)
|
*
|
* @author rk
|
* @date 2026/06/24
|
*/
|
@Slf4j
|
@Api(tags = "抖音")
|
@RestController
|
@RequestMapping("/web/douyin")
|
public class DouyinApi extends ApiController {
|
|
@Autowired
|
private DouyinProductService douyinProductService;
|
@Autowired
|
private DouyinVerifyService douyinVerifyService;
|
@Autowired
|
private DouyinVerifyLogService douyinVerifyLogService;
|
/** 抖音 HTTP 客户端:门店查询测试用 */
|
@Autowired
|
private DouyinClient douyinClient;
|
|
@ApiOperation(value = "联调测试:从抖音全量同步商品入库(返回入库条数)", notes = "小程序端")
|
@GetMapping("/testQuery")
|
public ApiResponse<Integer> testQuery() {
|
// 全量翻页拉取 online/query 并 upsert 本地商品 + SKU,返回本次入库条数
|
return ApiResponse.success(douyinProductService.syncFromDouyin());
|
}
|
|
@ApiOperation(value = "联调测试:查询抖音商户下门店ID列表(验证门店查询配置)", notes = "account_id 从字典读取")
|
@GetMapping("/testPoiList")
|
public ApiResponse<List<String>> testPoiList() {
|
// 门店查询为无状态透传,直接调 DouyinClient;account_id 由 Client 从字典读取
|
DouyinBaseResp<DouyinShopPoiResp> resp = douyinClient.shopPoiQuery();
|
List<DouyinShopPoiResp.Poi> pois = resp == null || resp.getData() == null ? null : resp.getData().getPois();
|
if (pois == null || pois.isEmpty()) {
|
return ApiResponse.success(Collections.emptyList());
|
}
|
// 仅提取门店ID,过滤 poi 节点或 poiId 为空的条目
|
List<String> poiIds = pois.stream()
|
.filter(p -> p != null && p.getPoi() != null && StringUtils.isNotBlank(p.getPoi().getPoiId()))
|
.map(p -> p.getPoi().getPoiId())
|
.collect(Collectors.toList());
|
return ApiResponse.success(poiIds);
|
}
|
|
@LoginRequired
|
@PreventRepeat
|
@ApiOperation("扫码一步核销(验券准备 + 核销合并;前端只调此接口)")
|
@PostMapping("/scanVerify")
|
public ApiResponse<DouyinVerifyRecord> scanVerify(@RequestBody DouyinPrepareParam param) {
|
String apiPath = "/web/douyin/scanVerify";
|
String memberId = getMemberId();
|
|
// ① 验券准备:扫码/输码 → 拿 verifyToken 与券列表(单独记一条 PREPARE 日志)
|
long prepareStart = System.currentTimeMillis();
|
DouyinVerifyLog prepareLog = baseLog(Constants.DOUYIN_VERIFY_OPERATE_TYPE.PREPARE.getKey(), apiPath, prepareStart);
|
prepareLog.setRawRequest(JSON.toJSONString(param));
|
if (param != null) {
|
prepareLog.setPoiId(param.getPoiId());
|
prepareLog.setOriginCode(StringUtils.firstNonBlank(param.getCode(), param.getQrContent()));
|
}
|
DouyinBaseResp<DouyinPrepareResp> prepareResp;
|
try {
|
prepareResp = douyinVerifyService.prepare(param);
|
prepareLog.setRawResponse(JSON.toJSONString(prepareResp));
|
Integer code = prepareResp == null || prepareResp.getExtra() == null ? null : prepareResp.getExtra().getErrorCode();
|
prepareLog.setResult(code != null && code == 0 ? Constants.DOUYIN_VERIFY_LOG_RESULT.SUCCESS.getKey() : Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
|
if (code == null || code != 0) {
|
String desc = prepareResp == null || prepareResp.getExtra() == null ? "无响应" : prepareResp.getExtra().getDescription();
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "验券准备失败:" + desc);
|
}
|
} catch (Throwable e) {
|
prepareLog.setResult(Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
|
prepareLog.setErrorMsg(e.getMessage());
|
throw e;
|
} finally {
|
saveLog(prepareLog);
|
}
|
|
// ② 取首张可核销券(canVerifyStatus=1 优先,否则首张),提取核销所需标识
|
DouyinPrepareResp prepareData = prepareResp.getData();
|
List<DouyinPrepareResp.Certificate> certificates = prepareData == null ? null : prepareData.getCertificates();
|
if (certificates == null || certificates.isEmpty()) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到可核销的券");
|
}
|
DouyinPrepareResp.Certificate cert = pickFirstVerifiable(certificates);
|
if (cert == null || cert.getSku() == null || StringUtils.isBlank(cert.getSku().getSkuId())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "券缺少 SKU 信息,无法核销");
|
}
|
|
// ③ 组装核销入参:verifyToken / 首张加密券码 / 门店 / skuId / 实付金额快照
|
DouyinVerifyParam verifyParam = new DouyinVerifyParam();
|
verifyParam.setVerifyToken(prepareData.getVerifyToken());
|
verifyParam.setPoiId(param == null ? null : param.getPoiId());
|
verifyParam.setEncryptedCodes(Collections.singletonList(cert.getEncryptedCode()));
|
verifyParam.setSkuId(cert.getSku().getSkuId());
|
verifyParam.setPayAmount(cert.getAmount() == null ? null : cert.getAmount().getPayAmount());
|
|
// ④ 核销 + 开套餐(单独记一条 VERIFY 日志)
|
long verifyStart = System.currentTimeMillis();
|
DouyinVerifyLog verifyLog = baseLog(Constants.DOUYIN_VERIFY_OPERATE_TYPE.VERIFY.getKey(), apiPath, verifyStart);
|
verifyLog.setRawRequest(JSON.toJSONString(verifyParam));
|
verifyLog.setPoiId(verifyParam.getPoiId());
|
try {
|
DouyinVerifyRecord rec = douyinVerifyService.verify(verifyParam, memberId);
|
fillByRecord(verifyLog, rec);
|
return ApiResponse.success(rec);
|
} catch (Throwable e) {
|
verifyLog.setResult(Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
|
verifyLog.setErrorMsg(e.getMessage());
|
throw e;
|
} finally {
|
saveLog(verifyLog);
|
}
|
}
|
|
/**
|
* 从券列表挑首张可核销的券(canVerifyStatus==1);都不满足则取首张,交给抖音核销接口判定。
|
*
|
* @param certificates prepare 返回的券列表(已保证非空)
|
* @return 首张可核销券;均不可核销时返回首张
|
*/
|
private DouyinPrepareResp.Certificate pickFirstVerifiable(List<DouyinPrepareResp.Certificate> certificates) {
|
for (DouyinPrepareResp.Certificate cert : certificates) {
|
if (cert != null && Constants.equalsInteger(cert.getCanVerifyStatus(), Constants.ONE)) {
|
return cert;
|
}
|
}
|
return certificates.get(0);
|
}
|
|
@LoginRequired
|
@ApiOperation("核销记录分页")
|
@PostMapping("/page")
|
public ApiResponse<PageData<DouyinVerifyRecord>> findPage(@RequestBody PageWrap<DouyinVerifyRecord> pageWrap) {
|
return ApiResponse.success(douyinVerifyService.findPage(pageWrap));
|
}
|
|
@LoginRequired
|
@ApiOperation("核销记录详情")
|
@GetMapping("/{id}")
|
public ApiResponse<DouyinVerifyRecord> findById(@PathVariable String id) {
|
return ApiResponse.success(douyinVerifyService.findById(id));
|
}
|
|
// ---------------- 操作日志辅助 ----------------
|
|
private DouyinVerifyLog baseLog(int operateType, String apiPath, long start) {
|
DouyinVerifyLog l = new DouyinVerifyLog();
|
l.setId(ID.nextGUID());
|
l.setOperateType(operateType);
|
l.setApiPath(apiPath);
|
l.setMemberId(getMemberId());
|
l.setIp(getRequest().getRemoteAddr());
|
l.setCostMs((int) (System.currentTimeMillis() - start));
|
l.setCreateDate(new Date());
|
l.setIsdeleted(Constants.ZERO);
|
return l;
|
}
|
|
/** verify 成功后,用核销记录回填日志的业务字段与结果(撤销核销已迁移至管理端) */
|
private void fillByRecord(DouyinVerifyLog opLog, DouyinVerifyRecord rec) {
|
if (rec == null) {
|
opLog.setResult(Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
|
return;
|
}
|
opLog.setVerifyRecordId(rec.getId());
|
if (StringUtils.isNotBlank(rec.getPoiId())) {
|
opLog.setPoiId(rec.getPoiId());
|
}
|
opLog.setOriginCode(rec.getOriginCode());
|
opLog.setRawResponse(rec.getRawResponse());
|
opLog.setResult(Constants.equalsInteger(rec.getVerifyStatus(), Constants.ZERO) ? Constants.DOUYIN_VERIFY_LOG_RESULT.SUCCESS.getKey() : Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
|
opLog.setErrorMsg(rec.getVerifyMsg());
|
}
|
|
/** 落库操作日志;日志自身异常不抛出,避免影响主流程 */
|
private void saveLog(DouyinVerifyLog opLog) {
|
try {
|
douyinVerifyLogService.record(opLog);
|
} catch (Exception e) {
|
log.warn("记录抖音验券操作日志失败 type={}, recordId={}", opLog.getOperateType(), opLog.getVerifyRecordId(), e);
|
}
|
}
|
}
|