liu
2026-09-30 91491239c5d4611b5b1404a7abeb2bb23d0edad5
yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java
@@ -30,6 +30,7 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -56,6 +57,16 @@
     */
    public static final String SIGNUP_RECORD_TYPE = "wxcp_activity_signup";
    /**
     * 活动封面的关联记录类型,对应 system_storage_attachment.record_type
     */
    public static final String ACTIVITY_RECORD_TYPE = "wxcp_activity";
    /**
     * 附件用途,与其它模块保持一致
     */
    private static final String ATTACHMENT_APPLICATION = "file";
    @Resource
    private WxCpActivityMapper wxcpActivityMapper;
    @Resource
@@ -72,6 +83,7 @@
    // ==================== 活动管理 ====================
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createActivity(WxCpActivitySaveReqVO createReqVO) {
        WxCpActivityDO activity = BeanUtils.toBean(createReqVO, WxCpActivityDO.class);
        // 新建的一律是草稿,避免未配置完就被扫码报名
@@ -83,10 +95,15 @@
            activity.setPushScope(WxCpPushScopeEnum.ALL.getScope());
        }
        wxcpActivityMapper.insert(activity);
        // 封面走 system 的附件中间表,业务表不落文件地址字段
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            bindActivityAttachments(activity.getId(), createReqVO.getBlobIds());
        }
        return activity.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateActivity(WxCpActivitySaveReqVO updateReqVO) {
        WxCpActivityDO activity = validateActivityExists(updateReqVO.getId());
        // 已结束或已取消的活动不再允许修改
@@ -101,12 +118,19 @@
        updateObj.setPushStatus(null);
        updateObj.setPushTime(null);
        wxcpActivityMapper.updateById(updateObj);
        // blobIds 为 null 表示请求里没带这个字段,视为不改动封面;传空数组才是明确要清空封面
        if (updateReqVO.getBlobIds() != null) {
            bindActivityAttachments(activity.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteActivity(Long id) {
        validateActivityExists(id);
        wxcpActivityMapper.deleteById(id);
        // 活动删了,封面附件关系一并清掉,避免残留成孤儿附件
        storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(ACTIVITY_RECORD_TYPE, id);
    }
    @Override
@@ -117,6 +141,53 @@
    @Override
    public PageResult<WxCpActivityDO> getActivityPage(WxCpActivityPageReqVO pageReqVO) {
        return wxcpActivityMapper.selectPage(pageReqVO);
    }
    @Override
    public List<SystemStorageBlobRespVO> getActivityAttachments(Long activityId) {
        return listAttachments(ACTIVITY_RECORD_TYPE, activityId);
    }
    /**
     * 绑定活动封面附件
     *
     * 走 system 模块的附件中间表(record_type = wxcp_activity),
     * 业务表不加文件地址列、不自建 upload 接口,符合项目统一文件管理规范。
     *
     * 传空集合会清掉关联记录但保留文件本身(bindAttachments 只删中间表记录)。
     * 注意不要改用 deleteAttachmentsByRecordTypeAndRecordId 来"先删后加":
     * 那个方法会连 blob 一起删掉,同一个文件重新绑定时会因为文件已不存在而查不出来。
     */
    private void bindActivityAttachments(Long activityId, List<Long> blobIds) {
        SystemStorageAttachmentSaveReqVO bindReqVO = new SystemStorageAttachmentSaveReqVO();
        bindReqVO.setApplication(ATTACHMENT_APPLICATION);
        bindReqVO.setRecordType(ACTIVITY_RECORD_TYPE);
        bindReqVO.setRecordId(activityId);
        bindReqVO.setBlobItems(toBlobItems(blobIds));
        storageAttachmentService.bindAttachments(bindReqVO);
    }
    private List<SystemStorageAttachmentSaveReqVO.BlobItem> toBlobItems(List<Long> blobIds) {
        if (CollUtil.isEmpty(blobIds)) {
            return Collections.emptyList();
        }
        return blobIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .map(blobId -> {
                    SystemStorageAttachmentSaveReqVO.BlobItem item = new SystemStorageAttachmentSaveReqVO.BlobItem();
                    item.setBlobId(blobId);
                    return item;
                })
                .toList();
    }
    private List<SystemStorageBlobRespVO> listAttachments(String recordType, Long recordId) {
        SystemStorageAttachmentListReqVO listReqVO = new SystemStorageAttachmentListReqVO();
        listReqVO.setApplication(ATTACHMENT_APPLICATION);
        listReqVO.setRecordType(recordType);
        listReqVO.setRecordId(recordId);
        return storageAttachmentService.listAttachments(listReqVO);
    }
    @Override
@@ -256,10 +327,16 @@
            throw exception(SIGNUP_FULL);
        }
        // 4. 去重:有企业微信账号按账号查,否则按手机号查
        WxCpActivitySignupDO exists = StrUtil.isNotBlank(reqVO.getWxUserId())
                ? wxcpSignupMapper.selectByActivityIdAndWxUserId(activity.getId(), reqVO.getWxUserId().trim())
                : wxcpSignupMapper.selectByActivityIdAndMobile(activity.getId(), reqVO.getMobile().trim());
        // 4. 去重:有企业微信账号按账号查;没有账号但有手机号则按手机号查;
        //    两者都没有时无法判定身份,不做去重——否则空手机号会互相误判成同一人
        String wxUserId = StrUtil.trimToEmpty(reqVO.getWxUserId());
        String mobile = StrUtil.trimToEmpty(reqVO.getMobile());
        WxCpActivitySignupDO exists = null;
        if (StrUtil.isNotBlank(wxUserId)) {
            exists = wxcpSignupMapper.selectByActivityIdAndWxUserId(activity.getId(), wxUserId);
        } else if (StrUtil.isNotBlank(mobile)) {
            exists = wxcpSignupMapper.selectByActivityIdAndMobile(activity.getId(), mobile);
        }
        if (exists != null && !Objects.equals(exists.getStatus(), WxCpSignupStatusEnum.CANCELED.getStatus())) {
            throw exception(SIGNUP_DUPLICATED);
        }
@@ -267,8 +344,8 @@
        if (exists != null) {
            wxcpSignupMapper.updateById(WxCpActivitySignupDO.builder()
                    .id(exists.getId())
                    .name(reqVO.getName())
                    .mobile(reqVO.getMobile())
                    .name(StrUtil.trimToEmpty(reqVO.getName()))
                    .mobile(mobile)
                    .deptName(reqVO.getDeptName())
                    .content(reqVO.getContent())
                    .remark(reqVO.getRemark())
@@ -276,9 +353,7 @@
                    .signupTime(now)
                    .checkInTime(null)
                    .build());
            // 重新报名时旧附件已失效,先清掉再绑新的
            storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(
                    SIGNUP_RECORD_TYPE, exists.getId());
            // 重新报名时旧附件作废,直接重新绑定(bindAttachments 会先清旧关联,且不会删掉文件本身)
            bindSignupAttachments(exists.getId(), reqVO.getBlobIds());
            incrementSignupCount(activity.getId());
            return exists.getId();
@@ -286,7 +361,10 @@
        // 5. 新增报名记录
        WxCpActivitySignupDO signup = BeanUtils.toBean(reqVO, WxCpActivitySignupDO.class);
        signup.setWxUserId(StrUtil.trimToEmpty(reqVO.getWxUserId()));
        signup.setWxUserId(wxUserId);
        // 姓名、手机号改为选填:统一存空串而不是 null,避免后续按手机号查重时空值语义混乱
        signup.setName(StrUtil.trimToEmpty(reqVO.getName()));
        signup.setMobile(mobile);
        signup.setStatus(WxCpSignupStatusEnum.SIGNED_UP.getStatus());
        signup.setSignupTime(now);
        wxcpSignupMapper.insert(signup);
@@ -300,36 +378,24 @@
    }
    /**
     * 绑定报名附件
     * 绑定报名附件(全量覆盖:先清掉该报名记录下的旧关联,再绑新的)
     *
     * 走 system 模块的附件中间表(record_type = wxcp_activity_signup),
     * 不新建 upload 接口、不落文件地址字段,符合项目统一文件管理规范。
     * 传空集合只清关联、不删文件本身。
     */
    private void bindSignupAttachments(Long signupId, List<Long> blobIds) {
        if (CollUtil.isEmpty(blobIds)) {
            return;
        }
        SystemStorageAttachmentSaveReqVO bindReqVO = new SystemStorageAttachmentSaveReqVO();
        bindReqVO.setApplication("file");
        bindReqVO.setApplication(ATTACHMENT_APPLICATION);
        bindReqVO.setRecordType(SIGNUP_RECORD_TYPE);
        bindReqVO.setRecordId(signupId);
        bindReqVO.setBlobItems(blobIds.stream()
                .filter(Objects::nonNull)
                .map(blobId -> {
                    SystemStorageAttachmentSaveReqVO.BlobItem item = new SystemStorageAttachmentSaveReqVO.BlobItem();
                    item.setBlobId(blobId);
                    return item;
                })
                .toList());
        bindReqVO.setBlobItems(toBlobItems(blobIds));
        storageAttachmentService.bindAttachments(bindReqVO);
    }
    @Override
    public List<SystemStorageBlobRespVO> getSignupAttachments(Long signupId) {
        SystemStorageAttachmentListReqVO listReqVO = new SystemStorageAttachmentListReqVO();
        listReqVO.setRecordType(SIGNUP_RECORD_TYPE);
        listReqVO.setRecordId(signupId);
        return storageAttachmentService.listAttachments(listReqVO);
        return listAttachments(SIGNUP_RECORD_TYPE, signupId);
    }
    private void incrementSignupCount(Long activityId) {