package com.doumee.api.business;
import com.alibaba.fastjson.JSON;
import com.doumee.api.BaseController;
import com.doumee.core.annotation.pr.PreventRepeat;
import com.doumee.core.constants.Constants;
import com.doumee.core.douyin.DouyinClient;
import com.doumee.core.douyin.dto.DouyinBaseResp;
import com.doumee.core.douyin.dto.DouyinCancelParam;
import com.doumee.core.douyin.dto.DouyinShopPoiResp;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.model.LoginUserInfo;
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.dao.business.vo.DouyinVerifyRecordPageVO;
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.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 抖音券核销(管理端):核销记录对外分页 + 撤销核销。
*
撤销核销由 web 端(/web/douyin/cancel)迁移至此,鉴权由 JWT 改为 Shiro;
* 管理端为运营补救场景,不受「核销后1小时内」限制。每次撤销落 douyin_verify_log 审计。
*
* @author rk
* @date 2026/06/26
*/
@Slf4j
@Api(tags = "抖音券核销")
@RestController
@RequestMapping("/business/douyinVerify")
public class DouyinVerifyController extends BaseController {
@Autowired
private DouyinVerifyService douyinVerifyService;
@Autowired
private DouyinVerifyLogService douyinVerifyLogService;
/** 抖音 HTTP 客户端:门店列表查询为无状态透传,直接调抖音,不经 Service */
@Autowired
private DouyinClient douyinClient;
@ApiOperation("核销记录分页(对外)")
@PostMapping("/page")
@RequiresPermissions("business:douyinVerify:query")
public ApiResponse> findPage(@RequestBody PageWrap pageWrap) {
return ApiResponse.success(douyinVerifyService.findManagePage(pageWrap));
}
@ApiOperation("查询抖音商户下门店(用于选核销门店;account_id 从字典读取)")
@PostMapping("/poiList")
@RequiresPermissions("business:douyinVerify:query")
public ApiResponse> poiList() {
// 门店查询为无状态透传(无落库),Controller → DouyinClient 直连;account_id 由 Client 从字典读取
DouyinBaseResp resp = douyinClient.shopPoiQuery();
List pois = resp == null || resp.getData() == null ? null : resp.getData().getPois();
if (pois == null || pois.isEmpty()) {
return ApiResponse.success(Collections.emptyList());
}
// 仅提取门店ID,过滤 poi 节点或 poiId 为空的条目
List 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);
}
@PreventRepeat
@ApiOperation("撤销核销(管理端,不受1小时限制;成功后作废本地套餐卡)")
@PostMapping("/cancel")
@RequiresPermissions("business:douyinVerify:cancel")
public ApiResponse cancel(@RequestBody DouyinCancelParam param, HttpServletRequest request) {
long start = System.currentTimeMillis();
DouyinVerifyLog opLog = baseLog(Constants.DOUYIN_VERIFY_OPERATE_TYPE.CANCEL.getKey(), "/business/douyinVerify/cancel", start, request);
opLog.setRawRequest(JSON.toJSONString(param));
try {
// 操作人取 Shiro 登录用户 id(管理端管理员,非会员)
LoginUserInfo loginUser = getLoginUser();
String operator = loginUser == null ? null : loginUser.getId();
DouyinVerifyRecord rec = douyinVerifyService.cancel(param, operator);
fillByRecord(opLog, rec);
return ApiResponse.success(rec);
} catch (Throwable e) {
opLog.setResult(Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
opLog.setErrorMsg(e.getMessage());
throw e;
} finally {
saveLog(opLog);
}
}
// ---------------- 撤销操作日志辅助 ----------------
/** 构造一条撤销操作日志骨架(主键/类型/路径/IP/耗时/时间) */
private DouyinVerifyLog baseLog(int operateType, String apiPath, long start, HttpServletRequest request) {
DouyinVerifyLog l = new DouyinVerifyLog();
l.setId(ID.nextGUID());
l.setOperateType(operateType);
l.setApiPath(apiPath);
// 管理端操作非会员,memberId 留空
l.setIp(request.getRemoteAddr());
l.setCostMs((int) (System.currentTimeMillis() - start));
l.setCreateDate(new Date());
l.setIsdeleted(Constants.ZERO);
return l;
}
/** 撤销成功后,用核销记录回填日志的业务字段与结果 */
private void fillByRecord(DouyinVerifyLog opLog, DouyinVerifyRecord rec) {
if (rec == null) {
opLog.setResult(Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
return;
}
opLog.setVerifyRecordId(rec.getId());
opLog.setOriginCode(rec.getOriginCode());
if (StringUtils.isNotBlank(rec.getPoiId())) {
opLog.setPoiId(rec.getPoiId());
}
opLog.setRawResponse(rec.getRawResponse());
opLog.setResult(Constants.equalsInteger(rec.getCancelStatus(), Constants.DOUYIN_VERIFY_CANCEL_STATUS.DONE.getKey()) ? Constants.DOUYIN_VERIFY_LOG_RESULT.SUCCESS.getKey() : Constants.DOUYIN_VERIFY_LOG_RESULT.FAIL.getKey());
opLog.setErrorMsg(rec.getCancelMsg());
}
/** 落库操作日志;日志自身异常不抛出,避免影响主流程 */
private void saveLog(DouyinVerifyLog opLog) {
try {
douyinVerifyLogService.record(opLog);
} catch (Exception e) {
log.warn("记录抖音撤销操作日志失败 type={}, recordId={}", opLog.getOperateType(), opLog.getVerifyRecordId(), e);
}
}
}