package com.microservices.zone.member.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.CacheConstants;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.core.utils.uuid.IdUtils;
import com.microservices.common.datascope.annotation.DataScope;
import com.microservices.common.redis.service.RedisService;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteCmsService;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysUser;
import com.microservices.system.api.utils.FeignUtils;
import com.microservices.zone.detail.service.IZoneDetailService;
import com.microservices.zone.member.domain.ZoneMember;
import com.microservices.zone.member.domain.points.ZonePointsAccount;
import com.microservices.zone.member.domain.points.ZonePointsApply;
import com.microservices.zone.member.domain.points.ZonePointsDoc;
import com.microservices.zone.member.domain.points.ZonePointsFailLog;
import com.microservices.zone.member.domain.points.ZonePointsTransaction;
import com.microservices.zone.member.domain.vo.ZonePointsAccountInputVo;
import com.microservices.zone.member.domain.vo.ZonePointsAccountUpdateVo;
import com.microservices.zone.member.domain.vo.ZonePointsApplyInputVo;
import com.microservices.zone.member.domain.vo.ZonePointsTransactionInputVo;
import com.microservices.zone.member.domain.vo.ZonePointsOverviewVo;
import com.microservices.zone.member.domain.vo.ZonePointsTopDocVo;
import com.microservices.zone.member.domain.vo.ZonePointsTopProjectVo;
import com.microservices.zone.member.mapper.ZoneMemberMapper;
import com.microservices.zone.member.mapper.ZonePointsAccountMapper;
import com.microservices.zone.member.mapper.ZonePointsApplyMapper;
import com.microservices.zone.member.mapper.ZonePointsDocMapper;
import com.microservices.zone.member.mapper.ZonePointsFailLogMapper;
import com.microservices.zone.member.mapper.ZonePointsTransactionMapper;
import com.microservices.zone.member.service.IZonePointsAccountService;
import com.microservices.zone.member.service.IZonePointsApplyService;
import com.microservices.zone.member.service.points.IPointsTaskHandler;
import com.microservices.zone.member.service.points.PointsAccountOperations;
import com.microservices.zone.member.service.points.PointsTaskContext;
import com.microservices.zone.project.domain.ZoneProject;
import com.microservices.zone.project.mapper.ZoneProjectMapper;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.utils.ZoneConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.time.Duration;

/**
 * 专区积分账户Service业务层处理
 *
 * @author otto
 */
@Service
public class ZonePointsAccountServiceImpl implements IZonePointsAccountService {
    private static final Logger logger = LoggerFactory.getLogger(ZonePointsAccountServiceImpl.class);

    private static final String STATUS_PENDING = "pending";
    @Autowired
    private ZonePointsAccountMapper zonePointsAccountMapper;

    @Autowired
    private IZoneDetailService zoneDetailService;

    @Autowired
    private RedisService redisService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RemoteUserService remoteUserService;

    @Autowired
    private ZoneMemberMapper zoneMemberMapper;

    @Autowired
    private ZonePointsFailLogMapper zonePointsFailLogMapper;

    @Autowired
    private ZonePointsApplyMapper zonePointsApplyMapper;

    @Autowired
    private PointsAccountOperations pointsAccountOperations;

    @Autowired
    private ZonePointsTransactionMapper zonePointsTransactionMapper;

    @Autowired
    private ZonePointsDocMapper zonePointsDocMapper;

    @Autowired
    private ZoneProjectMapper zoneProjectMapper;

    @Autowired
    private RemoteCmsService remoteCmsService;

    @Autowired
    private IZonePointsApplyService zonePointsApplyService;

    /** 策略模式：所有积分任务处理器，Spring 自动注入 */
    @Autowired
    private List<IPointsTaskHandler> pointsTaskHandlers;

    /** sourceType -> handler 快速查找表 */
    private final Map<String, IPointsTaskHandler> handlerMap = new HashMap<>();

    @PostConstruct
    public void initHandlers() {
        for (IPointsTaskHandler handler : pointsTaskHandlers) {
            handlerMap.put(handler.getSourceType(), handler);
        }
    }

    /**
     * 查询专区积分账户
     *
     * @param id 专区积分账户主键
     * @return 专区积分账户
     */
    @Override
    public ZonePointsAccount selectZonePointsAccountById(Long id) {
        ZonePointsAccount zonePointsAccount = zonePointsAccountMapper.selectZonePointsAccountById(id);
        if (zonePointsAccount == null) {
            throw new ServiceException("该积分账户不存在或无权限访问(积分账户id[" + id + "])");
        }
        zoneDetailService.checkZoneDataScope(zonePointsAccount.getZoneId());
        return zonePointsAccount;
    }

    /**
     * 查询专区积分账户列表
     *
     * @param zonePointsAccount 专区积分账户
     * @return 专区积分账户
     */
    @Override
    @DataScope(deptAlias = "zpa")
    public List<ZonePointsAccount> selectZonePointsAccountList(ZonePointsAccount zonePointsAccount) {
        return zonePointsAccountMapper.selectZonePointsAccountList(zonePointsAccount);
    }

    /**
     * 新增专区积分账户
     *
     * @param inputVo 专区积分账户
     * @return 结果
     */
    @Override
    public int insertZonePointsAccount(ZonePointsAccountInputVo inputVo) {
        // 检查是否已存在相同用户和专区的账户
        ZonePointsAccount existAccount = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(
                inputVo.getUserId(), inputVo.getZoneId());
        if (existAccount != null) {
            throw new ServiceException("该用户在该专区已存在积分账户");
        }
        ZonePointsAccount zonePointsAccount = inputVo.toZonePointsAccount();
        zoneDetailService.checkZoneDataScope(zonePointsAccount.getZoneId());
        // 设置组织ID
        if (zonePointsAccount.getDeptId() == null) {
            zonePointsAccount.setDeptId(zoneDetailService.selectZoneDetailById(zonePointsAccount.getZoneId()).getDeptId());
        }
        zonePointsAccount.setCreateTime(DateUtils.getNowDate());
        zonePointsAccount.setCreateBy(SecurityUtils.getUsername());
        return zonePointsAccountMapper.insertZonePointsAccount(zonePointsAccount);
    }

    /**
     * 修改专区积分账户
     *
     * @param updateVo 专区积分账户
     * @return 结果
     */
    @Override
    public int updateZonePointsAccount(ZonePointsAccountUpdateVo updateVo) {
        ZonePointsAccount oldAccount = selectZonePointsAccountById(updateVo.getId());
        ZonePointsAccount zonePointsAccount = updateVo.toZonePointsAccount();
        zonePointsAccount.setUpdateTime(DateUtils.getNowDate());
        zonePointsAccount.setUpdateBy(SecurityUtils.getUsername());
        return zonePointsAccountMapper.updateZonePointsAccount(zonePointsAccount);
    }

    /**
     * 批量删除专区积分账户
     *
     * @param ids 需要删除的专区积分账户主键
     * @return 结果
     */
    @Override
    public int deleteZonePointsAccountByIds(Long[] ids) {
        int success = 0;
        for (Long id : ids) {
            success += deleteZonePointsAccountById(id);
        }
        return success;
    }

    /**
     * 删除专区积分账户信息
     *
     * @param id 专区积分账户主键
     * @return 结果
     */
    @Override
    public int deleteZonePointsAccountById(Long id) {
        selectZonePointsAccountById(id);
        return zonePointsAccountMapper.deleteZonePointsAccountById(id);
    }

    /**
     * 根据用户ID和专区ID查询积分账户
     *
     * @param userId 用户ID
     * @param zoneId 专区ID
     * @return 专区积分账户
     */
    @Override
    public ZonePointsAccount selectZonePointsAccountByUserIdAndZoneId(Long userId, Long zoneId) {
        return zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
    }

    /**
     * 获取当前登录用户的积分账户总览信息
     *
     * @param zoneId 专区ID
     * @return 积分账户总览（包含累计获取积分、累计消费积分）
     */
    @Override
    public ZonePointsOverviewVo getCurrentUserPointsOverview(Long zoneId) {
        Long userId = SecurityUtils.getUserId();
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            ZonePointsOverviewVo overview = new ZonePointsOverviewVo();
            overview.setZoneId(zoneId);
            overview.setTotalEarnedPoints(0);
            overview.setTotalConsumedPoints(0);
            overview.setAvailablePoints(0);
            overview.setFrozenPoints(0);
            return overview;
        }

        ZonePointsOverviewVo overview = new ZonePointsOverviewVo();
        overview.setAccountId(account.getId());
        overview.setZoneId(zoneId);
        overview.setTotalEarnedPoints(account.getTotalEarnedPoints());
        overview.setTotalConsumedPoints(account.getTotalConsumedPoints());
        overview.setAvailablePoints(account.getAvailablePoints());
        overview.setFrozenPoints(account.getFrozenPoints());
        return overview;
    }

    /**
     * 获取当前登录用户的置顶文章信息
     *
     * @param zoneId 专区ID
     * @return 置顶文章信息（无置顶文章时返回null）
     */
    @Override
    public ZonePointsTopDocVo getCurrentUserPointsTopDoc(Long zoneId) {
        Long userId = SecurityUtils.getUserId();
        ZonePointsDoc pointsDoc = zonePointsDocMapper.selectPointsTopByZoneIdAndUserId(zoneId, userId);
        if (pointsDoc == null) {
            return null;
        }

        ZonePointsTopDocVo vo = new ZonePointsTopDocVo();
        vo.setId(pointsDoc.getId());
        vo.setZoneId(pointsDoc.getZoneId());
        vo.setDocId(pointsDoc.getDocId());
        vo.setIsPointsTop(pointsDoc.getIsPointsTop());
        vo.setExpiryTime(pointsDoc.getExpiryTime());

        // 通过 Feign 调用 CMS 获取文章名称
        try {
            JSONObject cmsDocJson = FeignUtils.getReturnData(
                    remoteCmsService.selectCmsDocJSONObjectById(pointsDoc.getDocId(), SecurityConstants.INNER));
            if (cmsDocJson != null) {
                vo.setDocName(cmsDocJson.getString("name"));
                vo.setSummary(cmsDocJson.getString("summary"));
            }
        } catch (Exception e) {
            logger.warn("获取置顶文章信息失败 docId={} error={}", pointsDoc.getDocId(), e.getMessage());
        }

        return vo;
    }

    /**
     * 查询专区下置顶文章列表（过期时间晚于当前时间，用于前台展示）
     *
     * @param zoneId 专区ID
     * @return 置顶文章列表（含文章名称和概要，通过 SQL 左联 cms_doc 获取）
     */
    @Override
    public List<ZonePointsTopDocVo> selectTopDocList(Long zoneId) {
        return zonePointsDocMapper.selectTopDocList(zoneId);
    }

    /**
     * 获取当前登录用户的置顶项目信息
     *
     * @param zoneId 专区ID
     * @return 置顶项目信息（无置顶项目时返回null）
     */
    @Override
    public ZonePointsTopProjectVo getZonePointsTopProject(Long zoneId) {
        String username = SecurityUtils.getUsername();
        ZoneProject project = zoneProjectMapper.selectPointsTopByZoneIdAndCreateBy(zoneId, username);
        if (project == null) {
            return null;
        }

        ZonePointsTopProjectVo vo = new ZonePointsTopProjectVo();
        vo.setId(project.getId());
        vo.setZoneId(project.getZoneId());
        vo.setName(project.getName());
        vo.setFullName(project.getFullName());
        vo.setGitlinkProjectId(project.getGitlinkProjectId());
        vo.setIsPointsTop(project.getIsPointsTop());
        vo.setExpiryTime(project.getExpiryTime());
        return vo;
    }

    /**
     * 根据会员ID查询积分账户列表
     *
     * @param memberId 专区会员ID
     * @return 专区积分账户列表
     */
    @Override
    public List<ZonePointsAccount> selectZonePointsAccountByMemberId(Long memberId) {
        return zonePointsAccountMapper.selectZonePointsAccountByMemberId(memberId);
    }

    @Async
    @Override
    public void createPointsAccountAsync(Long userId, Long zoneId, Long operatorId, Long deptId) {
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        if(!Objects.equals(deptId, cachedPointsDeptId)) {
            logger.info("积分-新增会员：非积分专区 userId={} deptId={} pointsDept={}", userId, deptId, cachedPointsDeptId);
            return;
        }
        JSONObject task = new JSONObject();
        task.put("sourceType", ZoneConstants.MemberAudit.SOURCE_TYPE);
        task.put("userId", userId);
        task.put("zoneId", zoneId);
        task.put("deptId", deptId);
        task.put("operatorId", operatorId);
        redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, JSON.toJSONString(task));
    }

    @Async
    @Override
    public void addDailyVisitPointsAsync(Long zoneId) {
        Long currentUserId = SecurityUtils.getUserId();
        ZoneMember zoneMember = zoneMemberMapper.selectZoneMember(zoneId, currentUserId, null);
        if (zoneMember == null || !ZoneConstants.MEMBER_AUDIT_PASS.equals(zoneMember.getIsAudit())) {
            logger.info("积分-每日登录：无专区会员信息 userId={} zoneId={}", currentUserId, zoneId);
            return;
        }

        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        Long deptId = zoneMember.getDeptId();
        if(!Objects.equals(deptId, cachedPointsDeptId)) {
            logger.info("积分-每日登录：非积分专区会员 userId={} deptId={} pointsDept={}", currentUserId, deptId, cachedPointsDeptId);
            return;
        }

        String redisKey = ZoneConstants.ZONE_DAILY_FIRST_VISIT_KEY + zoneId + ":" + currentUserId;

        // 使用 setIfAbsent 原子性地完成"判断是否存在 + 设置值 + 设置过期"，避免 increment 和 expire 两步非原子操作
        // 若服务在两步之间重启，key 将永不过期导致用户次日无法领取积分
        Boolean firstVisited = redisTemplate.opsForValue()
                .setIfAbsent(redisKey + ":counter", "1", Duration.ofSeconds(getSecondsToNextDay()));
        if (Boolean.FALSE.equals(firstVisited)) {
            logger.info("积分-每日登录：今日积分已领取 userId={} deptId={}", currentUserId, deptId);
            return;
        }

        // 写入异步队列
        JSONObject task = new JSONObject();
        task.put("sourceType", ZoneConstants.DailyVisit.SOURCE_TYPE);
        task.put("userId", currentUserId);
        task.put("zoneId", zoneId);
        task.put("deptId", deptId);
        task.put("operatorId", currentUserId);
        redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, JSON.toJSONString(task));
    }

    /**
     * 从 Redis 队列中消费异步任务（在定时任务中调用，主线程执行，事务正常生效）
     *
     * @return 处理的任务数量
     */
    @Override
    public int processPointsAsyncQueue() {
        int count = 0;
        while (true) {
            // 使用 5 秒超时避免 Redis 命令阻塞过久
            Object obj = redisService.rightPop(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, 5L, TimeUnit.SECONDS);

            if (obj == null) {
                break;
            }

            try {
                JSONObject task = JSON.parseObject(obj.toString());
                String sourceType = task.getString("sourceType");
                Long userId = task.getLong("userId");
                Long zoneId = task.getLong("zoneId");
                Long deptId = task.getLong("deptId");
                Long sourceId = task.getLong("sourceId");
                String sourceData = task.getString("sourceData");
                String subjectDesc = task.getString("subjectDesc");
                Long operatorId = task.getLong("operatorId");

                // 构建任务上下文并交给策略处理器
                PointsTaskContext ctx = new PointsTaskContext();
                ctx.setSourceType(sourceType);
                ctx.setUserId(userId);
                ctx.setZoneId(zoneId);
                ctx.setDeptId(deptId);
                ctx.setSourceId(sourceId);
                ctx.setSourceData(sourceData);
                ctx.setSubjectDesc(subjectDesc);
                ctx.setOperatorId(operatorId);

                IPointsTaskHandler handler = handlerMap.get(sourceType);
                if (handler != null) {
                    handler.handle(ctx);
                    count++;
                }
            } catch (Exception e) {
                // Handler 自带 @Transactional，异常时当前任务已独立回滚
                // 将任务放入重试队列，继续处理下一个任务
                redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_RETRY_KEY, obj.toString());
                logger.error("积分-异步任务：处理任务失败，已放入重试队列。继续处理下一个任务 task={} error={}", obj.toString(), e.getMessage());
            }
        }
        return count;
    }

    public ZonePointsAccount createPointsAccountTask(Long userId, Long zoneId, Long operatorId) {
        return pointsAccountOperations.createPointsAccountTask(userId, zoneId, operatorId);
    }

    private long getSecondsToNextDay() {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return (cal.getTimeInMillis() - System.currentTimeMillis()) / 1000;
    }

    /**
     * 记录积分变动（合并后只写 zone_points_transaction 单表）
     */
    private void recordPointsTransaction(Long accountId,
                                         String transactionType, int points,
                                         int availableAfter, int frozenAfter,
                                         String reason, Long operatorId, Long sourceId, String sourceType, String sourceData,
                                         String subject, String subjectDesc, String replyContent) {
        pointsAccountOperations.recordPointsTransaction(accountId, transactionType, points,
                availableAfter, frozenAfter, reason, operatorId, sourceId, sourceType, sourceData, subject, subjectDesc, replyContent);
    }

    @Async
    @Override
    public void addPublishResourcePointsAsync(ZoneResource zoneResource) {
        if (StringUtils.isEmpty(zoneResource.getCreateBy())) {
            logger.error("积分-资源发表：创建者信息为空 id={}", zoneResource.getId());
            return;
        }

        // 检查是否为积分专区（校验放在 Feign 调用前，非积分专区直接返回避免不必要的外部调用）
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        Long deptId = zoneDetailService.selectZoneDetailById(zoneResource.getZoneId()).getDeptId();
        if (!Objects.equals(deptId, cachedPointsDeptId)) {
            logger.info("积分-资源发表：非积分专区 zoneId={} deptId={} pointsDept={}", zoneResource.getZoneId(), deptId, cachedPointsDeptId);
            return;
        }

        SysUser createUser = FeignUtils.getReturnData(
                remoteUserService.getSysUserByUserName(zoneResource.getCreateBy(), SecurityConstants.INNER)
        );
        if (createUser == null) {
            logger.error("积分-资源发表：未获取到资源创建者信息 createBy={}", zoneResource.getCreateBy());
            return;
        }

        JSONObject task = new JSONObject();
        task.put("sourceType", ZoneConstants.PublishResource.SOURCE_TYPE);
        task.put("userId", createUser.getUserId());
        task.put("zoneId", zoneResource.getZoneId());
        task.put("deptId", deptId);
        task.put("sourceId", zoneResource.getId());
        task.put("subjectDesc", zoneResource.getName());
        task.put("operatorId", createUser.getUserId());
        redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, JSON.toJSONString(task));
    }

    @Async
    @Override
    public void addDownloadResourcePointsAsync(Long fileId, String sourceData) {
        JSONObject task = new JSONObject();
        task.put("sourceType", ZoneConstants.DownloadResource.SOURCE_TYPE);
        task.put("sourceId", fileId);
        task.put("sourceData", sourceData);
        redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, JSON.toJSONString(task));
        logger.info("积分-资源下载：已入队 fileId={} sourceData={}", fileId, sourceData);
    }

    @Override
    public void addRelevanceProjectPointsAsync(Long userId, Long zoneId, Long sourceId, String projectName, Long operatorId) {
        JSONObject task = new JSONObject();
        task.put("sourceType", ZoneConstants.RelevanceProject.SOURCE_TYPE);
        task.put("userId", userId);
        task.put("zoneId", zoneId);
        task.put("sourceId", sourceId);
        task.put("sourceData", null);
        task.put("subjectDesc", projectName);
        task.put("operatorId", operatorId);
        redisService.leftPush(ZoneConstants.ZONE_POINTS_ASYNC_QUEUE_KEY, JSON.toJSONString(task));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deductPointsForCertificate(ZonePointsTransactionInputVo inputVo) {
        Long userId = SecurityUtils.getUserId();
        Long zoneId = inputVo.getZoneId();
        String sourceData = inputVo.getSourceData();

        // ==================== 前置检查 ====================
        if(zoneId == null) {
            throw new ServiceException("专区信息丢失");
        }

        if (StringUtils.isBlank(sourceData)) {
            throw new ServiceException("证书信息丢失");
        }

        // 1. 是否为积分专区
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        ZoneMember member = zoneMemberMapper.selectZoneMember(zoneId, userId, null);
        if (member == null || member.getDeptId() == null
                || !Objects.equals(member.getDeptId(), cachedPointsDeptId)) {
            throw new ServiceException("当前专区不支持积分兑换电子证书");
        }

        // 2. 用户是否为普通会员（已审核通过）
        if (!ZoneConstants.MEMBER_AUDIT_PASS.equals(member.getIsAudit())) {
            throw new ServiceException("您还不是专区会员，无法兑换电子证书");
        }

        // 3. 是否已领取过证书（幂等保护）
        if (zonePointsTransactionMapper.countByEventTypeAndZoneAndUser(
                ZoneConstants.CertificateRedeem.SOURCE_TYPE, zoneId, userId) > 0) {
            throw new ServiceException("您已兑换过电子证书，无需重复兑换");
        }

        // 获取积分账户（必须有账户才能兑换，不再自动创建）
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            throw new ServiceException(String.format(
                    "您当前尚无积分（兑换电子证书需要 %d 积分），请先参与专区活动获取积分",
                    ZoneConstants.CertificateRedeem.POINTS));
        }

        // 4. 积分是否足够（原子扣减，WHERE 条件自带积分校验）
        int rows = zonePointsAccountMapper.deductPointsAtomic(account.getId(), ZoneConstants.CertificateRedeem.POINTS);
        if (rows == 0) {
            throw new ServiceException(String.format(
                    "积分不足，兑换电子证书需要 %d 积分，当前可用 %d 积分",
                    ZoneConstants.CertificateRedeem.POINTS, account.getAvailablePoints()));
        }

        // 记录积分流水（合并后只写 zone_points_transaction 单表）
        recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.DEDUCT, ZoneConstants.CertificateRedeem.POINTS,
                account.getAvailablePoints() - ZoneConstants.CertificateRedeem.POINTS, account.getFrozenPoints(),
                ZoneConstants.CertificateRedeem.REASON, userId, null, ZoneConstants.CertificateRedeem.SOURCE_TYPE, sourceData,
                ZoneConstants.CertificateRedeem.SUBJECT, ZoneConstants.CertificateRedeem.SUBJECT_DESC, null);

        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deductPointsForArticleTop(Long zoneId, Long id, String name) {
        Long userId = SecurityUtils.getUserId();

        // ==================== 前置检查 ====================

        // 1. 是否为积分专区
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        ZoneMember member = zoneMemberMapper.selectZoneMember(zoneId, userId, null);
        if (member == null || member.getDeptId() == null
                || !Objects.equals(member.getDeptId(), cachedPointsDeptId)) {
            throw new ServiceException("当前专区不支持文章置顶兑换");
        }

        // 2. 用户是否为普通会员（已审核通过）
        if (!ZoneConstants.MEMBER_AUDIT_PASS.equals(member.getIsAudit())) {
            throw new ServiceException("您还不是专区会员，无法兑换文章置顶");
        }

        // 3. 校验用户是否已经有置顶文章（zone_points_doc表 is_points_top）
        int topCount = zonePointsDocMapper.hasPointsTopByZoneIdAndUserId(zoneId, userId);
        if (topCount > 0) {
            throw new ServiceException("您已有置顶文章，每个用户仅允许置顶一篇文章");
        }

        // 获取积分账户（必须有账户才能兑换）
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            throw new ServiceException(String.format(
                    "您当前尚无积分（兑换文章置顶需要 %d 积分），请先参与专区活动获取积分",
                    ZoneConstants.ArticleTop.POINTS));
        }

        // 4. 积分是否足够（原子扣减，WHERE 条件自带积分校验）
        int rows = zonePointsAccountMapper.deductPointsAtomic(account.getId(), ZoneConstants.ArticleTop.POINTS);
        if (rows == 0) {
            throw new ServiceException(String.format(
                    "积分不足，兑换文章置顶需要 %d 积分，当前可用 %d 积分",
                    ZoneConstants.ArticleTop.POINTS, account.getAvailablePoints()));
        }

        // 5. 更新 zone_points_doc 表中对应文章（置顶状态、过期时间，如果没有数据则新增）
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_YEAR, 6);
        Date expiryTime = calendar.getTime();

        ZonePointsDoc existDoc = zonePointsDocMapper.selectZonePointsDocByZoneIdAndDocId(zoneId, id);
        if (existDoc != null) {
            existDoc.setIsPointsTop(true);
            existDoc.setExpiryTime(expiryTime);
            zonePointsDocMapper.updateZonePointsDoc(existDoc);
        } else {
            ZonePointsDoc newDoc = new ZonePointsDoc();
            newDoc.setZoneId(zoneId);
            newDoc.setDocId(id);
            newDoc.setIsPointsTop(true);
            newDoc.setExpiryTime(expiryTime);
            newDoc.setUserId(userId);
            newDoc.setCreateTime(DateUtils.getNowDate());
            zonePointsDocMapper.insertZonePointsDoc(newDoc);
        }

        // 计算到期日期字符串用于流水记录
        String expireDate = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime);

        // 记录积分流水（合并后只写 zone_points_transaction 单表）
        String subjectDesc = String.format(ZoneConstants.ArticleTop.SUBJECT_DESC_TEMPLATE, name);
        String reason = String.format(ZoneConstants.ArticleTop.REASON_TEMPLATE, expireDate);

        recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.DEDUCT, ZoneConstants.ArticleTop.POINTS,
                account.getAvailablePoints() - ZoneConstants.ArticleTop.POINTS, account.getFrozenPoints(),
                reason, userId, id, ZoneConstants.ArticleTop.SOURCE_TYPE, null,
                ZoneConstants.ArticleTop.SUBJECT, subjectDesc, null);

        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deductPointsForProjectTop(Long zoneId, Long id, String name) {
        Long userId = SecurityUtils.getUserId();

        // ==================== 前置检查 ====================

        // 1. 是否为积分专区
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        ZoneMember member = zoneMemberMapper.selectZoneMember(zoneId, userId, null);
        if (member == null || member.getDeptId() == null
                || !Objects.equals(member.getDeptId(), cachedPointsDeptId)) {
            throw new ServiceException("当前专区不支持项目置顶兑换");
        }

        // 2. 用户是否为普通会员（已审核通过）
        if (!ZoneConstants.MEMBER_AUDIT_PASS.equals(member.getIsAudit())) {
            throw new ServiceException("您还不是专区会员，无法兑换项目置顶");
        }

        // 3. 校验用户是否已经有置顶项目（zone_project 表 is_points_top）
        ZoneProject existTopProject = zoneProjectMapper.selectPointsTopByZoneIdAndCreateBy(zoneId, SecurityUtils.getUsername());
        if (existTopProject != null && !existTopProject.getId().equals(id)) {
            throw new ServiceException("您已有置顶项目，每个用户仅允许置顶一个项目");
        }

        // 获取积分账户（必须有账户才能兑换）
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            throw new ServiceException(String.format(
                    "您当前尚无积分（兑换项目置顶需要 %d 积分），请先参与专区活动获取积分",
                    ZoneConstants.ProjectTop.POINTS));
        }

        // 4. 积分是否足够（原子扣减，WHERE 条件自带积分校验）
        int rows = zonePointsAccountMapper.deductPointsAtomic(account.getId(), ZoneConstants.ProjectTop.POINTS);
        if (rows == 0) {
            throw new ServiceException(String.format(
                    "积分不足，兑换项目置顶需要 %d 积分，当前可用 %d 积分",
                    ZoneConstants.ProjectTop.POINTS, account.getAvailablePoints()));
        }

        // 5. 更新 zone_project 表中对应项目（置顶状态、过期时间）
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_YEAR, 6);
        Date expiryTime = calendar.getTime();

        ZoneProject project = zoneProjectMapper.selectZoneProjectById(id);
        if (project == null) {
            throw new ServiceException("项目不存在");
        }
        project.setIsPointsTop("1");
        project.setExpiryTime(expiryTime);
        zoneProjectMapper.updateZoneProject(project);

        // 计算到期日期字符串用于流水记录
        String expireDate = DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime);

        // 记录积分流水（合并后只写 zone_points_transaction 单表）
        String projectName = StringUtils.isNotBlank(name) ? name : project.getName();
        String subjectDesc = String.format(ZoneConstants.ProjectTop.SUBJECT_DESC_TEMPLATE, projectName);
        String reason = String.format(ZoneConstants.ProjectTop.REASON_TEMPLATE, expireDate);

        recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.DEDUCT, ZoneConstants.ProjectTop.POINTS,
                account.getAvailablePoints() - ZoneConstants.ProjectTop.POINTS, account.getFrozenPoints(),
                reason, userId, id, ZoneConstants.ProjectTop.SOURCE_TYPE, null,
                ZoneConstants.ProjectTop.SUBJECT, subjectDesc, null);

        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void freezePointsForOfflineActivity(ZonePointsApplyInputVo inputVo) {
        Long userId = SecurityUtils.getUserId();
        Long zoneId = inputVo.getZoneId();

        // ==================== 前置检查（与电子证书保持一致） ====================

        // 1. 是否为积分专区
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        ZoneMember member = zoneMemberMapper.selectZoneMember(zoneId, userId, null);
        if (member == null || member.getDeptId() == null
                || !Objects.equals(member.getDeptId(), cachedPointsDeptId)) {
            throw new ServiceException("当前专区不支持线下沙龙报名");
        }

        // 2. 用户是否为普通会员（已审核通过）
        if (!ZoneConstants.MEMBER_AUDIT_PASS.equals(member.getIsAudit())) {
            throw new ServiceException("您还不是专区会员，无法报名线下沙龙");
        }

        // 3. 是否已申请过线下沙龙（幂等保护）
        if (zonePointsApplyService.hasPendingApply(userId, zoneId, ZoneConstants.OfflineActivity.SOURCE_TYPE)) {
            throw new ServiceException("您已报名过线下沙龙，请勿重复报名");
        }

        // 获取积分账户（必须有账户才能预约，不再自动创建）
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            throw new ServiceException(String.format(
                    "您当前尚无积分（报名线下沙龙需要 %d 积分），请先参与专区活动获取积分",
                    ZoneConstants.OfflineActivity.POINTS));
        }

        // 4. 积分是否足够（原子冻结，WHERE 条件自带积分校验）
        int rows = zonePointsAccountMapper.freezePointsAtomic(account.getId(), ZoneConstants.OfflineActivity.POINTS);
        if (rows == 0) {
            int availablePoints = account.getAvailablePoints();
            throw new ServiceException(String.format(
                    "积分不足，报名线下沙龙需要 %d 积分，当前可用 %d 积分",
                    ZoneConstants.OfflineActivity.POINTS, availablePoints));
        }

        // 预生成eventId，关联冻结事件与申请记录
        String eventId = IdUtils.simpleUUID();

        // 插入积分申请记录
        ZonePointsApply zonePointsApply = inputVo.toZonePointsApply();
        zonePointsApply.setUserId(userId);
        zonePointsApply.setApplyType(ZoneConstants.OfflineActivity.SOURCE_TYPE);
        zonePointsApply.setPoints(ZoneConstants.OfflineActivity.POINTS);
        zonePointsApply.setEventId(eventId);
        zonePointsApply.setCreateTime(DateUtils.getNowDate());
        zonePointsApply.setCreateBy(SecurityUtils.getUsername());
        // deptId 以后端 member 为准，防止客户端未传或传错导致 INSERT 失败
        zonePointsApply.setDeptId(member.getDeptId());
        zonePointsApplyMapper.insertZonePointsApply(zonePointsApply);

        // 记录积分明细
        ZonePointsAccount locked = zonePointsAccountMapper.selectZonePointsAccountByIdForUpdate(account.getId());
        pointsAccountOperations.recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.FREEZED, ZoneConstants.OfflineActivity.POINTS,
                locked.getAvailablePoints(), locked.getFrozenPoints(),
                ZoneConstants.OfflineActivity.REASON,
                userId, null, ZoneConstants.OfflineActivity.SOURCE_TYPE, null,
                ZoneConstants.OfflineActivity.SUBJECT, null, null,
                eventId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void freezePointsForExpertQa(ZonePointsApplyInputVo inputVo) {
        Long userId = SecurityUtils.getUserId();
        Long zoneId = inputVo.getZoneId();

        // ==================== 前置检查（与电子证书保持一致） ====================

        // 1. 是否为积分专区
        Long cachedPointsDeptId = redisService.getCacheObject(CacheConstants.ZONE_POINTS_DEPT_ID_KEY);
        ZoneMember member = zoneMemberMapper.selectZoneMember(zoneId, userId, null);
        if (member == null || member.getDeptId() == null
                || !Objects.equals(member.getDeptId(), cachedPointsDeptId)) {
            throw new ServiceException("当前专区不支持专家答疑预约");
        }

        // 2. 用户是否为普通会员（已审核通过）
        if (!ZoneConstants.MEMBER_AUDIT_PASS.equals(member.getIsAudit())) {
            throw new ServiceException("您还不是专区会员，无法预约专家答疑");
        }

        // 3. 是否已申请过专家答疑（幂等保护）
        if (zonePointsApplyService.hasPendingApply(userId, zoneId, ZoneConstants.ExpertQA.SOURCE_TYPE)) {
            throw new ServiceException("您已预约过专家答疑，请勿重复预约");
        }

        // 获取积分账户（必须有账户才能预约，不再自动创建）
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(userId, zoneId);
        if (account == null) {
            throw new ServiceException(String.format(
                    "您当前尚无积分（预约专家答疑需要 %d 积分），请先参与专区活动获取积分",
                    ZoneConstants.ExpertQA.POINTS));
        }

        // 4. 积分是否足够（原子冻结，WHERE 条件自带积分校验）
        int rows = zonePointsAccountMapper.freezePointsAtomic(account.getId(), ZoneConstants.ExpertQA.POINTS);
        if (rows == 0) {
            int availablePoints = account.getAvailablePoints();
            throw new ServiceException(String.format(
                    "积分不足，预约专家答疑需要 %d 积分，当前可用 %d 积分",
                    ZoneConstants.ExpertQA.POINTS, availablePoints));
        }

        // 预生成eventId，关联冻结事件与申请记录
        String eventId = IdUtils.simpleUUID();

        // 插入积分申请记录
        ZonePointsApply zonePointsApply = inputVo.toZonePointsApply();
        zonePointsApply.setUserId(userId);
        zonePointsApply.setApplyType(ZoneConstants.ExpertQA.SOURCE_TYPE);
        zonePointsApply.setPoints(ZoneConstants.ExpertQA.POINTS);
        zonePointsApply.setEventId(eventId);
        zonePointsApply.setCreateTime(DateUtils.getNowDate());
        zonePointsApply.setCreateBy(SecurityUtils.getUsername());
        // deptId 以后端 member 为准，防止客户端未传或传错导致 INSERT 失败
        zonePointsApply.setDeptId(member.getDeptId());
        zonePointsApplyMapper.insertZonePointsApply(zonePointsApply);

        // 记录积分明细
        ZonePointsAccount locked = zonePointsAccountMapper.selectZonePointsAccountByIdForUpdate(account.getId());
        pointsAccountOperations.recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.FREEZED, ZoneConstants.ExpertQA.POINTS,
                locked.getAvailablePoints(), locked.getFrozenPoints(),
                ZoneConstants.ExpertQA.REASON,
                userId, null, ZoneConstants.ExpertQA.SOURCE_TYPE, null,
                ZoneConstants.ExpertQA.SUBJECT, null, null,
                eventId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rejectPointsApply(ZonePointsApplyInputVo inputVo) {
        // 1. 参数校验：申请ID不能为空
        if (inputVo.getId() == null) {
            throw new ServiceException("申请记录ID不能为空");
        }

        // 2. 查询申请记录
        ZonePointsApply apply = zonePointsApplyMapper.selectZonePointsApplyById(inputVo.getId());
        if (apply == null) {
            throw new ServiceException("申请记录不存在");
        }
        if (!"pending".equals(apply.getStatus())) {
            throw new ServiceException("当前申请状态不允许驳回，仅支持驳回待处理状态的申请");
        }

        Long currentUserId = SecurityUtils.getUserId();

        // 3. 更新申请记录状态为fail，记录驳回原因
        apply.setStatus("fail");
        apply.setReply(inputVo.getReply());
        apply.setReplyId(currentUserId);
        apply.setUpdateTime(DateUtils.getNowDate());
        zonePointsApplyMapper.updateZonePointsApply(apply);

        // 4. 查询积分账户
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(
                apply.getUserId(), apply.getZoneId());
        if (account == null) {
            throw new ServiceException("积分账户不存在");
        }

        // 5. 原子解冻积分
        int unfreezeRows = zonePointsAccountMapper.unfreezePointsAtomic(account.getId(), apply.getPoints());
        if (unfreezeRows == 0) {
            throw new ServiceException("积分解冻失败，冻结积分不足");
        }

        // 6. 重新获取解冻后的账户信息
        ZonePointsAccount unlocked = zonePointsAccountMapper.selectZonePointsAccountByIdForUpdate(account.getId());

        // 7. 记录积分解冻流水（合并后新增一条 unfreeze 记录，包含退回信息；关联已有的 eventId）
        String eventId = apply.getEventId() != null ? apply.getEventId() : IdUtils.simpleUUID();
        // 获取原始冻结交易的 subject，替换括号内容为"已退回"
        String rejectSubject = "已退回";
        if (apply.getEventId() != null) {
            List<ZonePointsTransaction> originTxList = zonePointsTransactionMapper
                    .selectZonePointsTransactionByEventId(apply.getEventId());
            if (originTxList != null && !originTxList.isEmpty()) {
                ZonePointsTransaction originTx = originTxList.get(0);
                if (originTx.getSubject() != null) {
                    rejectSubject = originTx.getSubject().replaceAll("（[^）]*）", "（已退回）");
                }
            }
        }
        pointsAccountOperations.recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.UNFREEZED, apply.getPoints(),
                unlocked.getAvailablePoints(), unlocked.getFrozenPoints(),
                "申请失败，冻结积分已全额退回",
                currentUserId, null, apply.getApplyType(), null,
                rejectSubject, null, inputVo.getReply(),
                eventId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approvePointsApply(ZonePointsApplyInputVo inputVo) {
        // 1. 参数校验：申请ID不能为空
        if (inputVo.getId() == null) {
            throw new ServiceException("申请记录ID不能为空");
        }

        // 2. 查询申请记录
        ZonePointsApply apply = zonePointsApplyMapper.selectZonePointsApplyById(inputVo.getId());
        if (apply == null) {
            throw new ServiceException("申请记录不存在");
        }
        if (!"pending".equals(apply.getStatus())) {
            throw new ServiceException("当前申请状态不允许通过，仅支持通过待处理状态的申请");
        }

        Long currentUserId = SecurityUtils.getUserId();

        // 3. 更新申请记录状态为success，记录回复
        apply.setStatus("success");
        apply.setReply(inputVo.getReply());
        apply.setReplyId(currentUserId);
        apply.setUpdateTime(DateUtils.getNowDate());
        zonePointsApplyMapper.updateZonePointsApply(apply);

        // 4. 查询积分账户
        ZonePointsAccount account = zonePointsAccountMapper.selectZonePointsAccountByUserIdAndZoneId(
                apply.getUserId(), apply.getZoneId());
        if (account == null) {
            throw new ServiceException("积分账户不存在");
        }

        // 5. 从冻结积分中原子扣减（含总积分扣减）
        int deductRows = zonePointsAccountMapper.deductFrozenPointsAtomic(account.getId(), apply.getPoints());
        if (deductRows == 0) {
            throw new ServiceException("冻结积分扣减失败，冻结积分不足");
        }

        // 6. 重新获取扣减后的账户信息
        ZonePointsAccount unlocked = zonePointsAccountMapper.selectZonePointsAccountByIdForUpdate(account.getId());

        // 7. 记录积分扣减流水（合并后新增一条 deduct 记录，包含完成信息；关联已有的 eventId）
        String eventId = apply.getEventId() != null ? apply.getEventId() : IdUtils.simpleUUID();
        // 获取原始冻结交易的 subject，去除括号内容
        String approveSubject = "已完成";
        if (apply.getEventId() != null) {
            List<ZonePointsTransaction> originTxList = zonePointsTransactionMapper
                    .selectZonePointsTransactionByEventId(apply.getEventId());
            if (originTxList != null && !originTxList.isEmpty()) {
                ZonePointsTransaction originTx = originTxList.get(0);
                if (originTx.getSubject() != null) {
                    approveSubject = originTx.getSubject().replaceAll("（[^）]*）", "");
                }
            }
        }
        pointsAccountOperations.recordPointsTransaction(account.getId(),
                ZoneConstants.TransactionType.DEDUCT, apply.getPoints(),
                unlocked.getAvailablePoints(), unlocked.getFrozenPoints(),
                "活动已结束，积分已成功扣除",
                currentUserId, null, apply.getApplyType(), null,
                approveSubject, null, inputVo.getReply(),
                eventId);
    }

    @Override
    public void saveFailLog(String taskData, String lastError) {
        ZonePointsFailLog failLog = new ZonePointsFailLog();
        failLog.setTaskData(taskData);
        failLog.setFailTime(new Date());
        failLog.setLastError(lastError);
        failLog.setStatus(STATUS_PENDING);
        failLog.setCreateTime(new Date());
        try {
            zonePointsFailLogMapper.insertZonePointsFailLog(failLog);
        } catch (Exception e) {
            logger.error("落库积分异步任务失败记录异常 task={} error={}", taskData, e.getMessage(), e);
        }
    }

}