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); } } }