From 5d34bdd3c426bb2ca5f6627424ae67984f745603 Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期三, 21 十月 2026 18:28:59 +0800
Subject: [PATCH] feat: 新增资产/用车/采购模块,完善课题、会员、财务与企微功能

---
 yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java |  547 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 515 insertions(+), 32 deletions(-)

diff --git a/yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java b/yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java
index 70508df..c8bdb5c 100644
--- a/yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java
+++ b/yudao-module-wxcp/src/main/java/cn/iocoder/yudao/module/wxcp/service/activity/WxCpActivityServiceImpl.java
@@ -7,13 +7,20 @@
 import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentListReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO;
 import cn.iocoder.yudao.module.system.service.storage.SystemStorageAttachmentService;
+import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService;
 import cn.iocoder.yudao.module.wxcp.controller.admin.activity.vo.*;
 import cn.iocoder.yudao.module.wxcp.dal.dataobject.activity.WxCpActivityDO;
+import cn.iocoder.yudao.module.wxcp.dal.dataobject.activity.WxCpActivityFieldDO;
 import cn.iocoder.yudao.module.wxcp.dal.dataobject.activity.WxCpActivitySignupDO;
+import cn.iocoder.yudao.module.wxcp.dal.dataobject.activity.WxCpActivitySignupValueDO;
+import cn.iocoder.yudao.module.wxcp.dal.mysql.activity.WxCpActivityFieldMapper;
 import cn.iocoder.yudao.module.wxcp.dal.mysql.activity.WxCpActivityMapper;
 import cn.iocoder.yudao.module.wxcp.dal.mysql.activity.WxCpActivitySignupMapper;
+import cn.iocoder.yudao.module.wxcp.dal.mysql.activity.WxCpActivitySignupValueMapper;
 import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpActivityStatusEnum;
+import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpActivityTypeEnum;
 import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpPushScopeEnum;
 import cn.iocoder.yudao.module.wxcp.enums.activity.WxCpSignupStatusEnum;
 import cn.iocoder.yudao.module.wxcp.controller.admin.vo.WxCpPushReqVO;
@@ -28,13 +35,19 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
+import java.io.File;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.wxcp.enums.ErrorCodeConstants.*;
@@ -52,17 +65,66 @@
     private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
 
     /**
+     * 浼佷笟寰俊鏂囨湰娑堟伅绾� 2048 瀛楄妭锛屼腑鏂囨寜 3 瀛楄妭绠楋紝杩欓噷鎸夊瓧绗︽暟鐣欎綑閲�
+     */
+    private static final int MAX_PUSH_CONTENT_LENGTH = 600;
+
+    /**
      * 鎶ュ悕闄勪欢鐨勫叧鑱旇褰曠被鍨嬶紝瀵瑰簲 system_storage_attachment.record_type
      */
     public static final String SIGNUP_RECORD_TYPE = "wxcp_activity_signup";
+
+    /**
+     * 娲诲姩灏侀潰鐨勫叧鑱旇褰曠被鍨嬶紝瀵瑰簲 system_storage_attachment.record_type
+     */
+    public static final String ACTIVITY_RECORD_TYPE = "wxcp_activity";
+
+    /**
+     * 娲诲姩璇︽儏鍥剧墖鐨勫叧鑱旇褰曠被鍨嬶紝瀵瑰簲 system_storage_attachment.record_type
+     *
+     * 涓庡皝闈㈠垎寮�瀛橈細灏侀潰鍙厑璁� 1 寮犮�佹姤鍚嶉〉椤堕儴瑁佸壀灞曠ず锛�
+     * 璇︽儏鍥惧彲浠ュ寮犮�佹姤鍚嶉〉鎸夊師鍥炬瘮渚嬪畬鏁村睍绀猴紙浜岀淮鐮�/娴锋姤杩欑被涓嶈兘瑁佺殑鍥炬斁杩欓噷锛夈��
+     */
+    public static final String ACTIVITY_DETAIL_RECORD_TYPE = "wxcp_activity_detail";
+
+    /**
+     * 鍥剧墖閫氱煡妯℃澘鐨勫浘鐗囧叧鑱旇褰曠被鍨�
+     */
+    public static final String ACTIVITY_IMAGE_RECORD_TYPE = "wxcp_activity_image";
+
+    /**
+     * 闄勪欢閫氱煡妯℃澘鐨勯檮浠跺叧鑱旇褰曠被鍨�
+     */
+    public static final String ACTIVITY_FILE_RECORD_TYPE = "wxcp_activity_file";
+
+    /**
+     * 闄勪欢鐢ㄩ�旓紝涓庡叾瀹冩ā鍧椾繚鎸佷竴鑷�
+     */
+    private static final String ATTACHMENT_APPLICATION = "file";
+
+    /**
+     * 浼佷笟寰俊鏂囦欢娑堟伅涓婇檺 20MB
+     */
+    private static final long MAX_FILE_SIZE = 20L * 1024L * 1024L;
+
+    /**
+     * 榛樿璇勪环鏈�楂樻槦绾�
+     */
+    private static final int DEFAULT_RATING_MAX = 5;
 
     @Resource
     private WxCpActivityMapper wxcpActivityMapper;
     @Resource
     private WxCpActivitySignupMapper wxcpSignupMapper;
+    @Resource
+    private WxCpActivityFieldMapper activityFieldMapper;
+    @Resource
+    private WxCpActivitySignupValueMapper signupValueMapper;
 
     @Resource
     private SystemStorageAttachmentService storageAttachmentService;
+    @Resource
+    private SystemStorageBlobService storageBlobService;
 
     @Resource
     private WxCpMessagePushService messagePushService;
@@ -72,21 +134,59 @@
     // ==================== 娲诲姩绠$悊 ====================
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Long createActivity(WxCpActivitySaveReqVO createReqVO) {
         WxCpActivityDO activity = BeanUtils.toBean(createReqVO, WxCpActivityDO.class);
         // 鏂板缓鐨勪竴寰嬫槸鑽夌锛岄伩鍏嶆湭閰嶇疆瀹屽氨琚壂鐮佹姤鍚�
         activity.setStatus(WxCpActivityStatusEnum.DRAFT.getStatus());
         activity.setSignupCount(0);
         activity.setPushStatus(0);
+        // 妯℃澘绫诲瀷锛氶粯璁ゃ�屾暟鎹噰闆�/璇勪环銆嶏紝涓庢敼閫犲墠鐨勬姤鍚嶆椿鍔ㄨ涓轰繚鎸佷竴鑷�
+        if (activity.getActivityType() == null) {
+            activity.setActivityType(WxCpActivityTypeEnum.FORM.getType());
+        }
+        if (activity.getRatingMax() == null || activity.getRatingMax() <= 0) {
+            activity.setRatingMax(DEFAULT_RATING_MAX);
+        }
+        if (activity.getRatingEnabled() == null) {
+            activity.setRatingEnabled(Boolean.FALSE);
+        }
+        // 鎶ュ悕涓婇檺鍙鏁版嵁閲囬泦妯℃澘鏈夋剰涔夛紱鍏跺畠妯℃澘涓嶄紶鏃舵寜涓嶉檺澶勭悊锛岄伩鍏嶆帴鍙e己鏍¢獙
+        if (activity.getSignupLimit() == null) {
+            activity.setSignupLimit(0);
+        }
+        if (StrUtil.isBlank(activity.getButtonText())) {
+            activity.setButtonText(defaultButtonText(activity.getActivityType()));
+        }
         // 鎺ㄩ�佽寖鍥撮�夊~锛屼笉浼犻粯璁ゅ叏鍛橈紱鎺ㄩ�佹椂杩樿兘鍦ㄥ脊绐楅噷鏀�
         if (activity.getPushScope() == null) {
             activity.setPushScope(WxCpPushScopeEnum.ALL.getScope());
         }
         wxcpActivityMapper.insert(activity);
+        // 灏侀潰璧� system 鐨勯檮浠朵腑闂磋〃锛屼笟鍔¤〃涓嶈惤鏂囦欢鍦板潃瀛楁
+        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
+            bindActivityAttachments(activity.getId(), createReqVO.getBlobIds());
+        }
+        // 璇︽儏鍥剧墖鍚屾牱璧伴檮浠朵腑闂磋〃锛屼笌灏侀潰鍒嗗紑瀛橈紝浜掍笉褰卞搷
+        if (CollUtil.isNotEmpty(createReqVO.getDetailBlobIds())) {
+            bindActivityDetailAttachments(activity.getId(), createReqVO.getDetailBlobIds());
+        }
+        // 鍥剧墖閫氱煡 / 闄勪欢閫氱煡鐨勭礌鏉愪篃璧伴檮浠朵腑闂磋〃锛屽悇鑷嫭绔� recordType
+        if (CollUtil.isNotEmpty(createReqVO.getImageBlobIds())) {
+            bindAttachments(ACTIVITY_IMAGE_RECORD_TYPE, activity.getId(), createReqVO.getImageBlobIds());
+        }
+        if (CollUtil.isNotEmpty(createReqVO.getFileBlobIds())) {
+            bindAttachments(ACTIVITY_FILE_RECORD_TYPE, activity.getId(), createReqVO.getFileBlobIds());
+        }
+        // 鏁版嵁閲囬泦妯℃澘鐨勮嚜瀹氫箟瀛楁
+        if (CollUtil.isNotEmpty(createReqVO.getFieldList())) {
+            saveActivityFields(activity.getId(), createReqVO.getFieldList());
+        }
         return activity.getId();
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void updateActivity(WxCpActivitySaveReqVO updateReqVO) {
         WxCpActivityDO activity = validateActivityExists(updateReqVO.getId());
         // 宸茬粨鏉熸垨宸插彇娑堢殑娲诲姩涓嶅啀鍏佽淇敼
@@ -101,12 +201,41 @@
         updateObj.setPushStatus(null);
         updateObj.setPushTime(null);
         wxcpActivityMapper.updateById(updateObj);
+        // blobIds 涓� null 琛ㄧず璇锋眰閲屾病甯﹁繖涓瓧娈碉紝瑙嗕负涓嶆敼鍔ㄥ皝闈紱浼犵┖鏁扮粍鎵嶆槸鏄庣‘瑕佹竻绌哄皝闈�
+        if (updateReqVO.getBlobIds() != null) {
+            bindActivityAttachments(activity.getId(), updateReqVO.getBlobIds());
+        }
+        // 璇︽儏鍥剧墖鍚岀悊锛歯ull = 涓嶆敼鍔紝绌烘暟缁� = 娓呯┖
+        if (updateReqVO.getDetailBlobIds() != null) {
+            bindActivityDetailAttachments(activity.getId(), updateReqVO.getDetailBlobIds());
+        }
+        if (updateReqVO.getImageBlobIds() != null) {
+            bindAttachments(ACTIVITY_IMAGE_RECORD_TYPE, activity.getId(), updateReqVO.getImageBlobIds());
+        }
+        if (updateReqVO.getFileBlobIds() != null) {
+            bindAttachments(ACTIVITY_FILE_RECORD_TYPE, activity.getId(), updateReqVO.getFileBlobIds());
+        }
+        // 瀛楁鍒楄〃锛歯ull = 涓嶆敼鍔紝绌烘暟缁� = 娓呯┖
+        if (updateReqVO.getFieldList() != null) {
+            activityFieldMapper.deleteByActivityId(activity.getId());
+            if (CollUtil.isNotEmpty(updateReqVO.getFieldList())) {
+                saveActivityFields(activity.getId(), updateReqVO.getFieldList());
+            }
+        }
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void deleteActivity(Long id) {
         validateActivityExists(id);
         wxcpActivityMapper.deleteById(id);
+        // 娲诲姩鍒犱簡锛屾墍鏈夐檮浠跺叧绯讳笌閲囬泦瀛楁涓�骞舵竻鎺夛紝閬垮厤娈嬬暀鎴愬鍎挎暟鎹�
+        storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(ACTIVITY_RECORD_TYPE, id);
+        storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(ACTIVITY_DETAIL_RECORD_TYPE, id);
+        storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(ACTIVITY_IMAGE_RECORD_TYPE, id);
+        storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(ACTIVITY_FILE_RECORD_TYPE, id);
+        activityFieldMapper.deleteByActivityId(id);
+        signupValueMapper.deleteByActivityId(id);
     }
 
     @Override
@@ -117,6 +246,127 @@
     @Override
     public PageResult<WxCpActivityDO> getActivityPage(WxCpActivityPageReqVO pageReqVO) {
         return wxcpActivityMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<SystemStorageBlobRespVO> getActivityAttachments(Long activityId) {
+        return listAttachments(ACTIVITY_RECORD_TYPE, activityId);
+    }
+
+    @Override
+    public List<SystemStorageBlobRespVO> getActivityDetailAttachments(Long activityId) {
+        List<SystemStorageBlobRespVO> attachments = listAttachments(ACTIVITY_DETAIL_RECORD_TYPE, activityId);
+        // 澶氬紶鍥惧繀椤讳繚璇佸睍绀洪『搴忕ǔ瀹氾細搴曞眰鏄� selectByIds锛屼笉鎵胯杩斿洖椤哄簭锛�
+        // 杩欓噷鎸� blobId 鍗囧簭锛堝嵆涓婁紶鍏堝悗锛夋帓涓�閬嶏紝閬垮厤鍚屼竴鎵瑰浘姣忔鍒锋柊鎹綅缃��
+        return attachments.stream()
+                .sorted(Comparator.comparing(SystemStorageBlobRespVO::getId,
+                        Comparator.nullsLast(Comparator.naturalOrder())))
+                .toList();
+    }
+
+    @Override
+    public List<SystemStorageBlobRespVO> getActivityImageAttachments(Long activityId) {
+        return listAttachments(ACTIVITY_IMAGE_RECORD_TYPE, activityId);
+    }
+
+    @Override
+    public List<SystemStorageBlobRespVO> getActivityFileAttachments(Long activityId) {
+        return listAttachments(ACTIVITY_FILE_RECORD_TYPE, activityId);
+    }
+
+    @Override
+    public List<WxCpActivityFieldDO> getActivityFields(Long activityId) {
+        return activityFieldMapper.selectListByActivityId(activityId);
+    }
+
+    @Override
+    public List<WxCpActivitySignupValueDO> getSignupValues(Long signupId) {
+        return signupValueMapper.selectListBySignupId(signupId);
+    }
+
+    /**
+     * 淇濆瓨娲诲姩鑷畾涔夐噰闆嗗瓧娈�
+     *
+     * 鍏堟暣浣撴竻绌哄啀鎻掑叆锛屼繚璇佸墠绔彁浜ょ殑椤哄簭涓庡唴瀹瑰氨鏄渶缁堢粨鏋滐紱
+     * 宸蹭骇鐢熺殑鎶ュ悕鍊煎仛瀛楁鍚�/绫诲瀷蹇収锛屼笉浼氬洜涓鸿繖閲岄噸寤哄瓧娈佃�屼涪澶卞惈涔夈��
+     */
+    private void saveActivityFields(Long activityId, List<WxCpActivityFieldSaveReqVO> fieldList) {
+        int index = 1;
+        for (WxCpActivityFieldSaveReqVO field : fieldList) {
+            if (field == null || StrUtil.isBlank(field.getFieldLabel())) {
+                throw exception(ACTIVITY_FIELD_LABEL_REQUIRED, index);
+            }
+            String fieldType = StrUtil.blankToDefault(field.getFieldType(), WxCpActivityFieldDO.TYPE_TEXT);
+            boolean needOptions = WxCpActivityFieldDO.TYPE_SELECT.equals(fieldType)
+                    || WxCpActivityFieldDO.TYPE_RADIO.equals(fieldType)
+                    || WxCpActivityFieldDO.TYPE_CHECKBOX.equals(fieldType);
+            if (needOptions && StrUtil.isBlank(field.getFieldOptions())) {
+                throw exception(ACTIVITY_FIELD_OPTIONS_REQUIRED, field.getFieldLabel());
+            }
+            activityFieldMapper.insert(WxCpActivityFieldDO.builder()
+                    .activityId(activityId)
+                    .fieldLabel(StrUtil.trim(field.getFieldLabel()))
+                    .fieldType(fieldType)
+                    .fieldOptions(StrUtil.trim(field.getFieldOptions()))
+                    .required(Boolean.TRUE.equals(field.getRequired()))
+                    .placeholder(StrUtil.trim(field.getPlaceholder()))
+                    .sortNo(field.getSortNo() != null ? field.getSortNo() : index)
+                    .build());
+            index++;
+        }
+    }
+
+    /**
+     * 缁戝畾娲诲姩灏侀潰闄勪欢
+     *
+     * 璧� system 妯″潡鐨勯檮浠朵腑闂磋〃锛坮ecord_type = wxcp_activity锛夛紝
+     * 涓氬姟琛ㄤ笉鍔犳枃浠跺湴鍧�鍒椼�佷笉鑷缓 upload 鎺ュ彛锛岀鍚堥」鐩粺涓�鏂囦欢绠$悊瑙勮寖銆�
+     *
+     * 浼犵┖闆嗗悎浼氭竻鎺夊叧鑱旇褰曚絾淇濈暀鏂囦欢鏈韩锛坆indAttachments 鍙垹涓棿琛ㄨ褰曪級銆�
+     * 娉ㄦ剰涓嶈鏀圭敤 deleteAttachmentsByRecordTypeAndRecordId 鏉�"鍏堝垹鍚庡姞"锛�
+     * 閭d釜鏂规硶浼氳繛 blob 涓�璧峰垹鎺夛紝鍚屼竴涓枃浠堕噸鏂扮粦瀹氭椂浼氬洜涓烘枃浠跺凡涓嶅瓨鍦ㄨ�屾煡涓嶅嚭鏉ャ��
+     */
+    private void bindActivityAttachments(Long activityId, List<Long> blobIds) {
+        bindAttachments(ACTIVITY_RECORD_TYPE, activityId, blobIds);
+    }
+
+    /**
+     * 缁戝畾娲诲姩璇︽儏鍥剧墖闄勪欢锛坮ecord_type = wxcp_activity_detail锛夛紝瑙勫垯鍚屽皝闈�
+     */
+    private void bindActivityDetailAttachments(Long activityId, List<Long> blobIds) {
+        bindAttachments(ACTIVITY_DETAIL_RECORD_TYPE, activityId, blobIds);
+    }
+
+    private void bindAttachments(String recordType, Long activityId, List<Long> blobIds) {
+        SystemStorageAttachmentSaveReqVO bindReqVO = new SystemStorageAttachmentSaveReqVO();
+        bindReqVO.setApplication(ATTACHMENT_APPLICATION);
+        bindReqVO.setRecordType(recordType);
+        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
@@ -164,10 +414,28 @@
             scope = WxCpPushScopeEnum.ALL.getScope();
         }
 
-        // 鏂囨湰鍗$墖娑堟伅锛氭寜閽洿杈炬姤鍚嶉〉锛岀偣娑堟伅鎴栨壂鐮侀兘鑳芥姤鍚�
-        WxCpPushResultVO result = messagePushService.pushCard(
-                activity.getTitle(), buildPushDescription(activity),
-                buildSignupUrl(activity.getId()), "绔嬪嵆鎶ュ悕", scope, target);
+        // 鎸夋椿鍔ㄦā鏉跨被鍨嬮�夋嫨鎺ㄩ�佸舰鎬�
+        Integer activityType = activity.getActivityType() != null
+                ? activity.getActivityType() : WxCpActivityTypeEnum.FORM.getType();
+        WxCpPushResultVO result;
+        if (Objects.equals(activityType, WxCpActivityTypeEnum.TEXT.getType())) {
+            result = messagePushService.pushText(buildTextContent(activity), scope, target);
+        } else if (Objects.equals(activityType, WxCpActivityTypeEnum.IMAGE.getType())) {
+            result = pushImages(activity, scope, target);
+        } else if (Objects.equals(activityType, WxCpActivityTypeEnum.FILE.getType())) {
+            result = pushFiles(activity, scope, target);
+        } else if (Objects.equals(activityType, WxCpActivityTypeEnum.MESSAGE.getType())) {
+            String url = tryBuildSignupUrl(activity.getId());
+            result = StrUtil.isBlank(url)
+                    ? messagePushService.pushText(buildTextContent(activity), scope, target)
+                    : messagePushService.pushCard(activity.getTitle(), buildMessageDescription(activity),
+                    url, pushButtonText(activity), scope, target);
+        } else {
+            // 鏁版嵁閲囬泦/璇勪环锛氬崱鐗囩洿杈鹃噰闆嗛〉锛岀偣娑堟伅鎴栨壂鐮侀兘鑳藉~鍐�
+            result = messagePushService.pushCard(
+                    activity.getTitle(), buildPushDescription(activity),
+                    buildSignupUrl(activity.getId()), pushButtonText(activity), scope, target);
+        }
 
         if (Boolean.TRUE.equals(result.getSuccess())) {
             wxcpActivityMapper.updateById(WxCpActivityDO.builder()
@@ -209,6 +477,150 @@
         return sb.toString();
     }
 
+    /**
+     * 绾枃鏈�/鍥剧墖/闄勪欢妯℃澘鐨勬鏂囷細鏍囬 + 鏃堕棿鍦扮偣 + 娲诲姩璇︽儏锛岃繃闀挎埅鏂伩鍏嶆暣鏉℃秷鎭彂閫佸け璐�
+     */
+    private String buildTextContent(WxCpActivityDO activity) {
+        StringBuilder body = new StringBuilder();
+        if (activity.getStartTime() != null) {
+            body.append("娲诲姩鏃堕棿锛�").append(activity.getStartTime().format(TIME_FMT));
+            if (activity.getEndTime() != null) {
+                body.append(" ~ ").append(activity.getEndTime().format(TIME_FMT));
+            }
+            body.append("\n");
+        }
+        if (StrUtil.isNotBlank(activity.getLocation())) {
+            body.append("娲诲姩鍦扮偣锛�").append(activity.getLocation()).append("\n");
+        }
+        if (StrUtil.isNotBlank(activity.getContent())) {
+            body.append(activity.getContent());
+        }
+        String content = "銆�" + activity.getTitle() + "銆慭n" + body.toString().trim();
+        if (content.length() > MAX_PUSH_CONTENT_LENGTH) {
+            content = content.substring(0, MAX_PUSH_CONTENT_LENGTH)
+                    + "\n鈥︹�︼紙鍐呭杩囬暱宸叉埅鏂紝璇峰埌绯荤粺鍐呮煡鐪嬪畬鏁村唴瀹癸級";
+        }
+        return content;
+    }
+
+    /**
+     * 鍗曟潯娑堟伅妯℃澘鐨勫崱鐗囨鏂囷細娲诲姩鎽樿 + 寮曞鐐瑰嚮
+     */
+    private String buildMessageDescription(WxCpActivityDO activity) {
+        String body = buildTextContent(activity);
+        // textcard 鐨� description 绾� 512 瀛楄妭锛岀暀鍑轰綑閲忥紝閬垮厤鏁存潯鍗$墖鍙戦�佸け璐�
+        if (body.length() > 300) {
+            body = body.substring(0, 300) + "鈥︹��";
+        }
+        return body + "\n鐐瑰嚮涓嬫柟鎸夐挳鏌ョ湅璇︽儏";
+    }
+
+    private String pushButtonText(WxCpActivityDO activity) {
+        return StrUtil.blankToDefault(activity.getButtonText(),
+                defaultButtonText(activity.getActivityType()));
+    }
+
+    private String defaultButtonText(Integer activityType) {
+        if (Objects.equals(activityType, WxCpActivityTypeEnum.MESSAGE.getType())) {
+            return "鏌ョ湅璇︽儏";
+        }
+        return "绔嬪嵆鎶ュ悕";
+    }
+
+    /**
+     * 鎷艰娲诲姩椤靛湴鍧�锛涙湭閰嶇疆澶栫綉鍓嶇紑鏃惰繑鍥� null锛岀敱璋冪敤鏂归檷绾т负鏂囨湰鎺ㄩ��
+     */
+    private String tryBuildSignupUrl(Long id) {
+        try {
+            return buildSignupUrl(id);
+        } catch (Exception e) {
+            log.warn("[tryBuildSignupUrl][鏈厤缃椿鍔ㄩ〉澶栫綉鍦板潃锛岄檷绾т负鏂囨湰鎺ㄩ�� id={}]", id);
+            return null;
+        }
+    }
+
+    /**
+     * 鍥剧墖閫氱煡锛氬厛鍙戞枃瀛楄鏄庯紝鍐嶉�愬紶鍙戦�佸浘鐗�
+     */
+    private WxCpPushResultVO pushImages(WxCpActivityDO activity, Integer scope, String target) {
+        List<SystemStorageBlobRespVO> images = getActivityImageAttachments(activity.getId());
+        if (CollUtil.isEmpty(images)) {
+            throw exception(ACTIVITY_IMAGE_REQUIRED);
+        }
+        List<WxCpPushResultVO> results = new ArrayList<>();
+        // 鏈夋鏂囧氨鍏堝彂涓�鏉℃枃瀛楄鏄庯紱鏂囧瓧澶辫触涓嶅奖鍝嶅悗缁浘鐗囧彂閫�
+        if (StrUtil.isNotBlank(activity.getContent())) {
+            results.add(messagePushService.pushText(buildTextContent(activity), scope, target));
+        }
+        for (SystemStorageBlobRespVO image : images) {
+            SystemStorageBlobDO blob = storageBlobService.getStorageBlob(image.getId());
+            if (blob == null) {
+                throw exception(ACTIVITY_FILE_NOT_EXISTS);
+            }
+            if (StrUtil.isNotBlank(blob.getContentType())
+                    && !StrUtil.startWithIgnoreCase(blob.getContentType(), "image/")) {
+                throw exception(ACTIVITY_IMAGE_ONLY);
+            }
+            File file = storageBlobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey());
+            if (file == null || !file.exists()) {
+                throw exception(ACTIVITY_FILE_NOT_EXISTS);
+            }
+            results.add(messagePushService.pushImage(file, scope, target));
+        }
+        return aggregatePushResults(results, "鍥剧墖");
+    }
+
+    /**
+     * 闄勪欢閫氱煡锛氬厛鍙戞枃瀛楄鏄庯紝鍐嶉�愪釜鍙戦�佹枃浠�
+     */
+    private WxCpPushResultVO pushFiles(WxCpActivityDO activity, Integer scope, String target) {
+        List<SystemStorageBlobRespVO> files = getActivityFileAttachments(activity.getId());
+        if (CollUtil.isEmpty(files)) {
+            throw exception(ACTIVITY_FILE_REQUIRED);
+        }
+        List<WxCpPushResultVO> results = new ArrayList<>();
+        if (StrUtil.isNotBlank(activity.getContent())) {
+            results.add(messagePushService.pushText(buildTextContent(activity), scope, target));
+        }
+        for (SystemStorageBlobRespVO item : files) {
+            SystemStorageBlobDO blob = storageBlobService.getStorageBlob(item.getId());
+            if (blob == null) {
+                throw exception(ACTIVITY_FILE_NOT_EXISTS);
+            }
+            if (blob.getByteSize() != null && blob.getByteSize() > MAX_FILE_SIZE) {
+                throw exception(ACTIVITY_FILE_TOO_LARGE);
+            }
+            File file = storageBlobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey());
+            if (file == null || !file.exists()) {
+                throw exception(ACTIVITY_FILE_NOT_EXISTS);
+            }
+            results.add(messagePushService.pushFile(file, blob.getOriginalFilename(), null, scope, target));
+        }
+        return aggregatePushResults(results, "闄勪欢");
+    }
+
+    /**
+     * 澶氭潯娑堟伅鍚堝苟缁撴灉锛氫换涓�鏉″け璐ュ嵆瑙嗕负鏁翠綋澶辫触锛屽苟鎶婂け璐ュ師鍥犳眹鎬荤粰鍓嶇
+     */
+    private WxCpPushResultVO aggregatePushResults(List<WxCpPushResultVO> results, String label) {
+        if (CollUtil.isEmpty(results)) {
+            return new WxCpPushResultVO(false, null, label + "鎺ㄩ�佸唴瀹逛负绌�", null, null);
+        }
+        List<WxCpPushResultVO> failures = results.stream()
+                .filter(r -> !Boolean.TRUE.equals(r.getSuccess()))
+                .toList();
+        if (failures.isEmpty()) {
+            return new WxCpPushResultVO(true, 0, "鎺ㄩ�佹垚鍔�", null, null);
+        }
+        String message = failures.stream()
+                .map(WxCpPushResultVO::getMessage)
+                .filter(StrUtil::isNotBlank)
+                .distinct()
+                .collect(Collectors.joining("锛�"));
+        return new WxCpPushResultVO(false, failures.get(0).getErrCode(),
+                label + "鎺ㄩ�佸け璐ワ細" + message, null, null);
+    }
+
     @Override
     public byte[] generateSignupQrCode(Long id, int size) {
         validateActivityExists(id);
@@ -240,6 +652,12 @@
         if (!Objects.equals(activity.getStatus(), WxCpActivityStatusEnum.PUBLISHED.getStatus())) {
             throw exception(SIGNUP_ACTIVITY_NOT_PUBLISHED);
         }
+        // 鍙湁銆屾暟鎹噰闆�/璇勪环銆嶆ā鏉挎湁琛ㄥ崟锛屽叾瀹冩ā鏉垮彧鍋氭秷鎭帹閫�
+        Integer activityType = activity.getActivityType() != null
+                ? activity.getActivityType() : WxCpActivityTypeEnum.FORM.getType();
+        if (!Objects.equals(activityType, WxCpActivityTypeEnum.FORM.getType())) {
+            throw exception(SIGNUP_NOT_SUPPORTED);
+        }
 
         // 2. 鏍¢獙鎶ュ悕鏃堕棿绐楀彛
         LocalDateTime now = LocalDateTime.now();
@@ -256,10 +674,22 @@
             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());
+        // 3.1 鏍¢獙鑷畾涔夐噰闆嗗瓧娈典笌璇勪环锛岄伩鍏嶆妸绌哄�煎啓杩涘簱
+        List<WxCpActivityFieldDO> fields = getActivityFields(activity.getId());
+        List<WxCpActivitySignupValueDO> fieldValues =
+                buildSignupValues(activity.getId(), fields, reqVO.getFieldValues());
+        validateRating(activity, reqVO.getRatingScore());
+
+        // 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,32 +697,38 @@
         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())
+                    .ratingScore(reqVO.getRatingScore())
                     .remark(reqVO.getRemark())
                     .status(WxCpSignupStatusEnum.SIGNED_UP.getStatus())
                     .signupTime(now)
                     .checkInTime(null)
                     .build());
-            // 閲嶆柊鎶ュ悕鏃舵棫闄勪欢宸插け鏁堬紝鍏堟竻鎺夊啀缁戞柊鐨�
-            storageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(
-                    SIGNUP_RECORD_TYPE, exists.getId());
+            // 閲嶆柊鎶ュ悕鏃舵棫闄勪欢/閲囬泦鍊间綔搴燂紝鎸夋湰娆℃彁浜よ鐩�
             bindSignupAttachments(exists.getId(), reqVO.getBlobIds());
+            signupValueMapper.deleteBySignupId(exists.getId());
+            insertSignupValues(fieldValues, exists.getId());
             incrementSignupCount(activity.getId());
             return exists.getId();
         }
 
         // 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.setRatingScore(reqVO.getRatingScore());
         signup.setStatus(WxCpSignupStatusEnum.SIGNED_UP.getStatus());
         signup.setSignupTime(now);
         wxcpSignupMapper.insert(signup);
 
-        // 6. 缁戝畾鎶ュ悕浜轰笂浼犵殑鍥剧墖/闄勪欢
+        // 6. 缁戝畾鎶ュ悕浜轰笂浼犵殑鍥剧墖/闄勪欢涓庨噰闆嗗瓧娈靛��
         bindSignupAttachments(signup.getId(), reqVO.getBlobIds());
+        insertSignupValues(fieldValues, signup.getId());
 
         // 7. 鐢� SQL 鑷锛岄伩鍏嶅苟鍙戜笅鐨勮鏀瑰啓涓㈠け鏇存柊
         incrementSignupCount(activity.getId());
@@ -300,36 +736,83 @@
     }
 
     /**
-     * 缁戝畾鎶ュ悕闄勪欢
+     * 缁戝畾鎶ュ悕闄勪欢锛堝叏閲忚鐩栵細鍏堟竻鎺夎鎶ュ悕璁板綍涓嬬殑鏃у叧鑱旓紝鍐嶇粦鏂扮殑锛�
      *
      * 璧� system 妯″潡鐨勯檮浠朵腑闂磋〃锛坮ecord_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 List<WxCpActivitySignupValueDO> buildSignupValues(Long activityId, List<WxCpActivityFieldDO> fields,
+                                                               List<WxCpSignupValueReqVO> reqValues) {
+        if (CollUtil.isEmpty(fields)) {
+            return Collections.emptyList();
+        }
+        Map<Long, String> valueMap = CollUtil.isEmpty(reqValues) ? Collections.emptyMap()
+                : reqValues.stream()
+                .filter(v -> v != null && v.getFieldId() != null)
+                .collect(Collectors.toMap(WxCpSignupValueReqVO::getFieldId,
+                        v -> StrUtil.trimToEmpty(v.getFieldValue()), (a, b) -> b));
+        List<WxCpActivitySignupValueDO> result = new ArrayList<>();
+        for (WxCpActivityFieldDO field : fields) {
+            String value = StrUtil.trimToEmpty(valueMap.get(field.getId()));
+            if (Boolean.TRUE.equals(field.getRequired()) && StrUtil.isBlank(value)) {
+                throw exception(SIGNUP_FIELD_REQUIRED, field.getFieldLabel());
+            }
+            if (StrUtil.isBlank(value)) {
+                continue;
+            }
+            result.add(WxCpActivitySignupValueDO.builder()
+                    .activityId(activityId)
+                    .fieldId(field.getId())
+                    .fieldLabel(field.getFieldLabel())
+                    .fieldType(field.getFieldType())
+                    .fieldValue(value)
+                    .sortNo(field.getSortNo())
+                    .build());
+        }
+        return result;
+    }
+
+    private void insertSignupValues(List<WxCpActivitySignupValueDO> values, Long signupId) {
+        if (CollUtil.isEmpty(values)) {
+            return;
+        }
+        values.forEach(value -> value.setSignupId(signupId));
+        signupValueMapper.insertBatch(values);
+    }
+
+    /**
+     * 寮�鍚瘎浠锋椂鏍¢獙鏄熺骇鑼冨洿
+     */
+    private void validateRating(WxCpActivityDO activity, Integer score) {
+        if (!Boolean.TRUE.equals(activity.getRatingEnabled())) {
+            return;
+        }
+        if (score == null) {
+            throw exception(SIGNUP_RATING_REQUIRED);
+        }
+        int max = activity.getRatingMax() != null && activity.getRatingMax() > 0
+                ? activity.getRatingMax() : DEFAULT_RATING_MAX;
+        if (score < 1 || score > max) {
+            throw exception(SIGNUP_RATING_INVALID, max);
+        }
     }
 
     private void incrementSignupCount(Long activityId) {

--
Gitblit v1.9.3