rk
5 天以前 84ae873e1c19ca7d2ffc5c98248285706dae818b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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;
 
/**
 * 抖音券核销(管理端):核销记录对外分页 + 撤销核销。
 * <p>撤销核销由 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<PageData<DouyinVerifyRecordPageVO>> findPage(@RequestBody PageWrap<DouyinVerifyRecordPageVO> pageWrap) {
        return ApiResponse.success(douyinVerifyService.findManagePage(pageWrap));
    }
 
    @ApiOperation("查询抖音商户下门店(用于选核销门店;account_id 从字典读取)")
    @PostMapping("/poiList")
    @RequiresPermissions("business:douyinVerify:query")
    public ApiResponse<List<String>> poiList() {
        // 门店查询为无状态透传(无落库),Controller → 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);
    }
 
    @PreventRepeat
    @ApiOperation("撤销核销(管理端,不受1小时限制;成功后作废本地套餐卡)")
    @PostMapping("/cancel")
    @RequiresPermissions("business:douyinVerify:cancel")
    public ApiResponse<DouyinVerifyRecord> 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);
        }
    }
}