diff --git a/backend/build/test-uploads/work-report-attachments/17-441493a9535a4812a7f198ab3983eccd.zip b/backend/build/test-uploads/work-report-attachments/17-441493a9535a4812a7f198ab3983eccd.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-441493a9535a4812a7f198ab3983eccd.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/build/test-uploads/work-report-attachments/17-9751a655ab214580af3c834ccec6f1b3.zip b/backend/build/test-uploads/work-report-attachments/17-9751a655ab214580af3c834ccec6f1b3.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-9751a655ab214580af3c834ccec6f1b3.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/build/test-uploads/work-report-attachments/17-d66dd072ba514f9e97dde4f2b834202b.zip b/backend/build/test-uploads/work-report-attachments/17-d66dd072ba514f9e97dde4f2b834202b.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-d66dd072ba514f9e97dde4f2b834202b.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java b/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java
index e57bc50d..c796b881 100644
--- a/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java
+++ b/backend/src/main/java/com/unis/crm/UnisCrmBackendApplication.java
@@ -3,6 +3,7 @@ package com.unis.crm;
 import com.unis.crm.config.WecomProperties;
 import com.unis.crm.config.InternalAuthProperties;
 import com.unis.crm.config.OmsProperties;
+import com.unis.crm.config.WorkReportProperties;
 import java.util.TimeZone;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
@@ -12,7 +13,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication(scanBasePackages = "com.unis.crm")
 @MapperScan({"com.unis.crm.mapper", "com.unis.crm.llm.mapper"})
-@EnableConfigurationProperties({WecomProperties.class, OmsProperties.class, InternalAuthProperties.class})
+@EnableConfigurationProperties({WecomProperties.class, OmsProperties.class, InternalAuthProperties.class, WorkReportProperties.class})
 @EnableScheduling
 public class UnisCrmBackendApplication {
 
diff --git a/backend/src/main/java/com/unis/crm/config/WorkReportProperties.java b/backend/src/main/java/com/unis/crm/config/WorkReportProperties.java
new file mode 100644
index 00000000..1f7d7f17
--- /dev/null
+++ b/backend/src/main/java/com/unis/crm/config/WorkReportProperties.java
@@ -0,0 +1,97 @@
+package com.unis.crm.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "unisbase.app.work-report")
+public class WorkReportProperties {
+
+    private DailyReport dailyReport = new DailyReport();
+
+    public DailyReport getDailyReport() {
+        return dailyReport;
+    }
+
+    public void setDailyReport(DailyReport dailyReport) {
+        this.dailyReport = dailyReport == null ? new DailyReport() : dailyReport;
+    }
+
+    public static class DailyReport {
+        private boolean requiredByDefault = true;
+        private List<String> requiredRoleCodes = new ArrayList<>();
+        private List<String> optionalRoleCodes = new ArrayList<>();
+        private FeatureVisibility notifyUsers = new FeatureVisibility();
+        private FeatureVisibility attachments = new FeatureVisibility();
+
+        public boolean isRequiredByDefault() {
+            return requiredByDefault;
+        }
+
+        public void setRequiredByDefault(boolean requiredByDefault) {
+            this.requiredByDefault = requiredByDefault;
+        }
+
+        public List<String> getRequiredRoleCodes() {
+            return requiredRoleCodes;
+        }
+
+        public void setRequiredRoleCodes(List<String> requiredRoleCodes) {
+            this.requiredRoleCodes = requiredRoleCodes == null ? new ArrayList<>() : requiredRoleCodes;
+        }
+
+        public List<String> getOptionalRoleCodes() {
+            return optionalRoleCodes;
+        }
+
+        public void setOptionalRoleCodes(List<String> optionalRoleCodes) {
+            this.optionalRoleCodes = optionalRoleCodes == null ? new ArrayList<>() : optionalRoleCodes;
+        }
+
+        public FeatureVisibility getNotifyUsers() {
+            return notifyUsers;
+        }
+
+        public void setNotifyUsers(FeatureVisibility notifyUsers) {
+            this.notifyUsers = notifyUsers == null ? new FeatureVisibility() : notifyUsers;
+        }
+
+        public FeatureVisibility getAttachments() {
+            return attachments;
+        }
+
+        public void setAttachments(FeatureVisibility attachments) {
+            this.attachments = attachments == null ? new FeatureVisibility() : attachments;
+        }
+    }
+
+    public static class FeatureVisibility {
+        private boolean visibleByDefault = true;
+        private List<String> visibleRoleCodes = new ArrayList<>();
+        private List<String> hiddenRoleCodes = new ArrayList<>();
+
+        public boolean isVisibleByDefault() {
+            return visibleByDefault;
+        }
+
+        public void setVisibleByDefault(boolean visibleByDefault) {
+            this.visibleByDefault = visibleByDefault;
+        }
+
+        public List<String> getVisibleRoleCodes() {
+            return visibleRoleCodes;
+        }
+
+        public void setVisibleRoleCodes(List<String> visibleRoleCodes) {
+            this.visibleRoleCodes = visibleRoleCodes == null ? new ArrayList<>() : visibleRoleCodes;
+        }
+
+        public List<String> getHiddenRoleCodes() {
+            return hiddenRoleCodes;
+        }
+
+        public void setHiddenRoleCodes(List<String> hiddenRoleCodes) {
+            this.hiddenRoleCodes = hiddenRoleCodes == null ? new ArrayList<>() : hiddenRoleCodes;
+        }
+    }
+}
diff --git a/backend/src/main/java/com/unis/crm/controller/WorkController.java b/backend/src/main/java/com/unis/crm/controller/WorkController.java
index 97139507..4ee4c053 100644
--- a/backend/src/main/java/com/unis/crm/controller/WorkController.java
+++ b/backend/src/main/java/com/unis/crm/controller/WorkController.java
@@ -7,6 +7,7 @@ import com.unis.crm.dto.work.CreateWorkCheckInRequest;
 import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
 import com.unis.crm.dto.work.WorkCheckInExportDTO;
 import com.unis.crm.dto.work.WorkDailyReportExportDTO;
+import com.unis.crm.dto.work.WorkDailyReportRequirementDTO;
 import com.unis.crm.dto.work.WorkHistoryItemDTO;
 import com.unis.crm.dto.work.WorkHistoryPageDTO;
 import com.unis.crm.dto.work.WorkOverviewDTO;
@@ -52,6 +53,11 @@ public class WorkController {
         return ApiResponse.success(workService.getOverview(CurrentUserUtils.requireCurrentUserId(userId)));
     }
 
+    @GetMapping("/daily-report-requirement")
+    public ApiResponse<WorkDailyReportRequirementDTO> getDailyReportRequirement(@RequestHeader("X-User-Id") Long userId) {
+        return ApiResponse.success(workService.getDailyReportRequirement(CurrentUserUtils.requireCurrentUserId(userId)));
+    }
+
     @GetMapping("/report-message-users")
     public ApiResponse<List<WorkReportToUserDTO>> listReportMessageUsers(
             @RequestHeader("X-User-Id") Long userId,
diff --git a/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java
index a95dfb72..41acb040 100644
--- a/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java
+++ b/backend/src/main/java/com/unis/crm/dto/work/CreateWorkDailyReportRequest.java
@@ -1,7 +1,5 @@
 package com.unis.crm.dto.work;
 
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.Size;
 import jakarta.validation.Valid;
 import java.util.ArrayList;
@@ -9,11 +7,9 @@ import java.util.List;
 
 public class CreateWorkDailyReportRequest {
 
-    @NotBlank(message = "今日工作内容不能为空")
     @Size(max = 4000, message = "今日工作内容不能超过4000字符")
     private String workContent;
 
-    @NotEmpty(message = "请至少填写一条今日工作内容")
     @Valid
     private List<WorkReportLineItemRequest> lineItems = new ArrayList<>();
 
diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportRequirementDTO.java b/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportRequirementDTO.java
new file mode 100644
index 00000000..5b257f23
--- /dev/null
+++ b/backend/src/main/java/com/unis/crm/dto/work/WorkDailyReportRequirementDTO.java
@@ -0,0 +1,41 @@
+package com.unis.crm.dto.work;
+
+public class WorkDailyReportRequirementDTO {
+
+    private boolean objectSelectionRequired;
+    private boolean notifyUsersVisible;
+    private boolean attachmentsVisible;
+
+    public WorkDailyReportRequirementDTO() {
+    }
+
+    public WorkDailyReportRequirementDTO(boolean objectSelectionRequired, boolean notifyUsersVisible, boolean attachmentsVisible) {
+        this.objectSelectionRequired = objectSelectionRequired;
+        this.notifyUsersVisible = notifyUsersVisible;
+        this.attachmentsVisible = attachmentsVisible;
+    }
+
+    public boolean isObjectSelectionRequired() {
+        return objectSelectionRequired;
+    }
+
+    public void setObjectSelectionRequired(boolean objectSelectionRequired) {
+        this.objectSelectionRequired = objectSelectionRequired;
+    }
+
+    public boolean isNotifyUsersVisible() {
+        return notifyUsersVisible;
+    }
+
+    public void setNotifyUsersVisible(boolean notifyUsersVisible) {
+        this.notifyUsersVisible = notifyUsersVisible;
+    }
+
+    public boolean isAttachmentsVisible() {
+        return attachmentsVisible;
+    }
+
+    public void setAttachmentsVisible(boolean attachmentsVisible) {
+        this.attachmentsVisible = attachmentsVisible;
+    }
+}
diff --git a/backend/src/main/java/com/unis/crm/dto/work/WorkReportLineItemRequest.java b/backend/src/main/java/com/unis/crm/dto/work/WorkReportLineItemRequest.java
index 50ce26dc..2a1d2237 100644
--- a/backend/src/main/java/com/unis/crm/dto/work/WorkReportLineItemRequest.java
+++ b/backend/src/main/java/com/unis/crm/dto/work/WorkReportLineItemRequest.java
@@ -1,6 +1,5 @@
 package com.unis.crm.dto.work;
 
-import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Size;
 import jakarta.validation.Valid;
 import java.util.ArrayList;
@@ -8,10 +7,8 @@ import java.util.List;
 
 public class WorkReportLineItemRequest {
 
-    @NotBlank(message = "工作日期不能为空")
     private String workDate;
 
-    @NotBlank(message = "跟进对象类型不能为空")
     private String bizType;
 
     private Long bizId;
@@ -19,11 +16,9 @@ public class WorkReportLineItemRequest {
     @Size(max = 200, message = "对象名称不能超过200字符")
     private String bizName;
 
-    @NotBlank(message = "日报内容不能为空")
     @Size(max = 4000, message = "日报内容不能超过4000字符")
     private String editorText;
 
-    @NotBlank(message = "工作内容不能为空")
     @Size(max = 1000, message = "工作内容不能超过1000字符")
     private String content;
     private String visitStartTime;
diff --git a/backend/src/main/java/com/unis/crm/service/WorkService.java b/backend/src/main/java/com/unis/crm/service/WorkService.java
index 498f39ad..3434a004 100644
--- a/backend/src/main/java/com/unis/crm/service/WorkService.java
+++ b/backend/src/main/java/com/unis/crm/service/WorkService.java
@@ -7,6 +7,7 @@ import com.unis.crm.dto.work.WorkDailyReportExportDTO;
 import com.unis.crm.dto.work.WorkHistoryItemDTO;
 import com.unis.crm.dto.work.WorkHistoryPageDTO;
 import com.unis.crm.dto.work.WorkOverviewDTO;
+import com.unis.crm.dto.work.WorkDailyReportRequirementDTO;
 import com.unis.crm.dto.work.WorkReportAttachmentDTO;
 import com.unis.crm.dto.work.WorkReportToUserDTO;
 import java.math.BigDecimal;
@@ -19,6 +20,8 @@ public interface WorkService {
 
     WorkOverviewDTO getOverview(Long userId);
 
+    WorkDailyReportRequirementDTO getDailyReportRequirement(Long userId);
+
     List<WorkReportToUserDTO> listReportMessageUsers(Long userId, String keyword);
 
     WorkHistoryPageDTO getHistory(Long userId, String type, int page, int size);
diff --git a/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java
index a0ea7b04..e78fad97 100644
--- a/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java
+++ b/backend/src/main/java/com/unis/crm/service/impl/WorkServiceImpl.java
@@ -3,12 +3,14 @@ package com.unis.crm.service.impl;
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.unis.crm.common.BusinessException;
+import com.unis.crm.config.WorkReportProperties;
 import com.unis.crm.dto.work.CreateWorkCheckInRequest;
 import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
 import com.unis.crm.dto.work.WorkCheckInDTO;
 import com.unis.crm.dto.work.WorkCheckInExportDTO;
 import com.unis.crm.dto.work.WorkDailyReportDTO;
 import com.unis.crm.dto.work.WorkDailyReportExportDTO;
+import com.unis.crm.dto.work.WorkDailyReportRequirementDTO;
 import com.unis.crm.dto.work.WorkHistoryItemDTO;
 import com.unis.crm.dto.work.WorkHistoryPageDTO;
 import com.unis.crm.dto.work.WorkReportLineItemDTO;
@@ -87,7 +89,7 @@ public class WorkServiceImpl implements WorkService {
     private static final String OPPORTUNITY_STAGE_LABEL = "项目阶段";
     private static final String OPPORTUNITY_STAGE_TYPE_CODE = "sj_xmjd";
     private static final int EXPORT_LIMIT = 5000;
-    private static final long REPORT_ATTACHMENT_MAX_SIZE_BYTES = 20L * 1024 * 1024;
+    private static final long REPORT_ATTACHMENT_MAX_SIZE_BYTES = 500L * 1024 * 1024;
     private static final String TENCENT_COORD_TYPE_GPS = "1";
     private static final String TENCENT_COORD_TYPE_GCJ02 = "2";
     private static final String OPEN_STREET_MAP_USER_AGENT = "unis-crm/1.0";
@@ -104,6 +106,7 @@ public class WorkServiceImpl implements WorkService {
     private final ObjectMapper objectMapper;
     private final UnisBaseTenantProvider tenantProvider;
     private final ReportReminderService reportReminderService;
+    private final WorkReportProperties workReportProperties;
     private final Path checkInPhotoDirectory;
     private final Path reportAttachmentDirectory;
     private final String tencentMapKey;
@@ -114,6 +117,7 @@ public class WorkServiceImpl implements WorkService {
             OpportunityMapper opportunityMapper,
             ProfileMapper profileMapper,
             ReportReminderService reportReminderService,
+            WorkReportProperties workReportProperties,
             ObjectMapper objectMapper,
             UnisBaseTenantProvider tenantProvider,
             @Value("${unisbase.app.upload-path}") String uploadPath,
@@ -122,6 +126,7 @@ public class WorkServiceImpl implements WorkService {
         this.opportunityMapper = opportunityMapper;
         this.profileMapper = profileMapper;
         this.reportReminderService = reportReminderService;
+        this.workReportProperties = workReportProperties == null ? new WorkReportProperties() : workReportProperties;
         this.objectMapper = objectMapper;
         this.tenantProvider = tenantProvider;
         this.httpClient = HttpClient.newBuilder()
@@ -146,9 +151,22 @@ public class WorkServiceImpl implements WorkService {
     @Override
     public List<WorkReportToUserDTO> listReportMessageUsers(Long userId, String keyword) {
         requireUser(userId);
+        if (!isDailyReportNotifyUsersVisible(loadUserRoleCodes(userId))) {
+            throw new BusinessException("当前角色不可使用日报通知用户功能");
+        }
         return workMapper.selectReportMessageUsers(resolveCurrentTenantId(), normalizeOptionalText(keyword), 50);
     }
 
+    @Override
+    public WorkDailyReportRequirementDTO getDailyReportRequirement(Long userId) {
+        requireUser(userId);
+        List<String> roleCodes = loadUserRoleCodes(userId);
+        return new WorkDailyReportRequirementDTO(
+                isDailyReportObjectSelectionRequired(roleCodes),
+                isDailyReportNotifyUsersVisible(roleCodes),
+                isDailyReportAttachmentsVisible(roleCodes));
+    }
+
     @Override
     public WorkHistoryPageDTO getHistory(Long userId, String type, int page, int size) {
         requireUser(userId);
@@ -272,9 +290,12 @@ public class WorkServiceImpl implements WorkService {
     @Transactional
     public Long saveDailyReport(Long userId, CreateWorkDailyReportRequest request) {
         requireUser(userId);
-        ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
-        normalizeDailyReportRequest(request);
-        request.setTomorrowPlan(request.getTomorrowPlan().trim());
+        List<String> roleCodes = ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
+        normalizeDailyReportRequest(
+                request,
+                isDailyReportObjectSelectionRequired(roleCodes),
+                isDailyReportNotifyUsersVisible(roleCodes),
+                isDailyReportAttachmentsVisible(roleCodes));
         request.setSourceType(normalizeOptionalText(request.getSourceType()));
         if (request.getSourceType() == null) {
             request.setSourceType("manual");
@@ -405,12 +426,15 @@ public class WorkServiceImpl implements WorkService {
     @Override
     public WorkReportAttachmentDTO uploadReportAttachment(Long userId, MultipartFile file) {
         requireUser(userId);
-        ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
+        List<String> roleCodes = ensureWorkWriteAllowed(userId, "当前角色仅可查看日报历史记录");
+        if (!isDailyReportAttachmentsVisible(roleCodes)) {
+            throw new BusinessException("当前角色不可使用日报附件功能");
+        }
         if (file == null || file.isEmpty()) {
             throw new BusinessException("请先选择附件");
         }
         if (file.getSize() > REPORT_ATTACHMENT_MAX_SIZE_BYTES) {
-            throw new BusinessException("单个附件不能超过20MB");
+            throw new BusinessException("单个附件不能超过500MB");
         }
 
         String originalName = normalizeAttachmentName(file.getOriginalFilename());
@@ -471,15 +495,77 @@ public class WorkServiceImpl implements WorkService {
         }
     }
 
-    private void ensureWorkWriteAllowed(Long userId, String deniedMessage) {
-        List<String> roleCodes = profileMapper.selectUserRoleCodes(userId);
+    private List<String> ensureWorkWriteAllowed(Long userId, String deniedMessage) {
+        List<String> roleCodes = loadUserRoleCodes(userId);
         boolean onlySee = roleCodes != null && roleCodes.stream()
-                .filter(roleCode -> roleCode != null && !roleCode.isBlank())
-                .map(roleCode -> roleCode.trim().toLowerCase())
                 .anyMatch(ONLY_SEE_ROLE_CODE::equals);
         if (onlySee) {
             throw new BusinessException(deniedMessage);
         }
+        return roleCodes;
+    }
+
+    private List<String> loadUserRoleCodes(Long userId) {
+        List<String> roleCodes = profileMapper.selectUserRoleCodes(userId);
+        if (roleCodes == null || roleCodes.isEmpty()) {
+            return List.of();
+        }
+        return roleCodes.stream()
+                .map(this::normalizeRoleCode)
+                .filter(roleCode -> roleCode != null)
+                .distinct()
+                .toList();
+    }
+
+    private boolean isDailyReportObjectSelectionRequired(List<String> roleCodes) {
+        WorkReportProperties.DailyReport dailyReport = workReportProperties.getDailyReport();
+        if (matchesConfiguredRole(roleCodes, dailyReport.getRequiredRoleCodes())) {
+            return true;
+        }
+        if (matchesConfiguredRole(roleCodes, dailyReport.getOptionalRoleCodes())) {
+            return false;
+        }
+        return dailyReport.isRequiredByDefault();
+    }
+
+    private boolean isDailyReportNotifyUsersVisible(List<String> roleCodes) {
+        return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getNotifyUsers());
+    }
+
+    private boolean isDailyReportAttachmentsVisible(List<String> roleCodes) {
+        return isFeatureVisible(roleCodes, workReportProperties.getDailyReport().getAttachments());
+    }
+
+    private boolean isFeatureVisible(List<String> roleCodes, WorkReportProperties.FeatureVisibility visibility) {
+        WorkReportProperties.FeatureVisibility safeVisibility = visibility == null
+                ? new WorkReportProperties.FeatureVisibility()
+                : visibility;
+        if (matchesConfiguredRole(roleCodes, safeVisibility.getHiddenRoleCodes())) {
+            return false;
+        }
+        if (matchesConfiguredRole(roleCodes, safeVisibility.getVisibleRoleCodes())) {
+            return true;
+        }
+        return safeVisibility.isVisibleByDefault();
+    }
+
+    private boolean matchesConfiguredRole(List<String> userRoleCodes, List<String> configuredRoleCodes) {
+        if (userRoleCodes == null || userRoleCodes.isEmpty() || configuredRoleCodes == null || configuredRoleCodes.isEmpty()) {
+            return false;
+        }
+        List<String> normalizedConfiguredRoles = configuredRoleCodes.stream()
+                .map(this::normalizeRoleCode)
+                .filter(roleCode -> roleCode != null)
+                .toList();
+        if (normalizedConfiguredRoles.isEmpty()) {
+            return false;
+        }
+        return userRoleCodes.stream().anyMatch(normalizedConfiguredRoles::contains);
+    }
+
+    private String normalizeRoleCode(String roleCode) {
+        String normalized = normalizeOptionalText(roleCode);
+        return normalized == null ? null : normalized.toLowerCase();
     }
 
     private List<WorkHistoryItemDTO> loadHistoryItems(HistoryType historyType, int fetchLimit) {
@@ -1056,9 +1142,14 @@ public class WorkServiceImpl implements WorkService {
         return new PlanItemMetadata(buffer.toString().trim(), planItems);
     }
 
-    private void normalizeDailyReportRequest(CreateWorkDailyReportRequest request) {
+    private void normalizeDailyReportRequest(
+            CreateWorkDailyReportRequest request,
+            boolean objectSelectionRequired,
+            boolean notifyUsersVisible,
+            boolean attachmentsVisible) {
         List<WorkReportLineItemRequest> normalizedItems = new ArrayList<>();
-        for (WorkReportLineItemRequest item : request.getLineItems()) {
+        List<WorkReportLineItemRequest> lineItems = request.getLineItems() == null ? List.of() : request.getLineItems();
+        for (WorkReportLineItemRequest item : lineItems) {
             if (item == null) {
                 continue;
             }
@@ -1073,18 +1164,26 @@ public class WorkServiceImpl implements WorkService {
             item.setStage(normalizeOptionalText(item.getStage()));
             item.setCommunicationTime(normalizeOptionalText(item.getCommunicationTime()));
             item.setCommunicationContent(normalizeOptionalText(item.getCommunicationContent()));
-            item.setAttachments(normalizeReportAttachments(item.getAttachments()));
-            item.setToUsers(normalizeReportToUsers(item.getToUsers()));
+            item.setAttachments(attachmentsVisible ? normalizeReportAttachments(item.getAttachments()) : List.of());
+            item.setToUsers(notifyUsersVisible ? normalizeReportToUsers(item.getToUsers()) : List.of());
             hydrateLineItemFromEditorText(item);
-            if (item.getWorkDate() == null || item.getContent() == null || item.getEditorText() == null) {
+            item.setContent(normalizeOptionalText(item.getContent()));
+            item.setEditorText(normalizeOptionalText(item.getEditorText()));
+            if (item.getWorkDate() == null || item.getContent() == null) {
                 throw new BusinessException("请完整填写每一条今日工作内容");
             }
+            if (objectSelectionRequired && !hasLinkedReportTarget(item)) {
+                throw new BusinessException("请使用 # 选择日报关联对象");
+            }
+            if (item.getEditorText() == null) {
+                item.setEditorText(item.getContent());
+            }
             normalizedItems.add(item);
         }
         if (normalizedItems.isEmpty()) {
             throw new BusinessException("请至少填写一条今日工作内容");
         }
-        request.setAttachments(normalizeReportAttachments(request.getAttachments()));
+        request.setAttachments(attachmentsVisible ? normalizeReportAttachments(request.getAttachments()) : List.of());
         List<WorkTomorrowPlanItemRequest> normalizedPlanItems = new ArrayList<>();
         List<WorkTomorrowPlanItemRequest> planItems = request.getPlanItems() == null ? List.of() : request.getPlanItems();
         for (WorkTomorrowPlanItemRequest item : planItems) {
@@ -1099,9 +1198,8 @@ public class WorkServiceImpl implements WorkService {
         }
         request.setLineItems(normalizedItems);
         request.setPlanItems(normalizedPlanItems);
-        request.setWorkContent(appendReportAttachmentMetadata(
-                appendReportLineMetadata(buildReportPlainText(normalizedItems), normalizedItems),
-                request.getAttachments()));
+        String normalizedWorkContent = appendReportLineMetadata(buildReportPlainText(normalizedItems), normalizedItems);
+        request.setWorkContent(appendReportAttachmentMetadata(normalizedWorkContent, request.getAttachments()));
         request.setTomorrowPlan(normalizedPlanItems.isEmpty()
                 ? ""
                 : appendPlanItemMetadata(buildTomorrowPlanPlainText(normalizedPlanItems), normalizedPlanItems));
diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml
index 04a895ab..af80c8b3 100644
--- a/backend/src/main/resources/application-prod.yml
+++ b/backend/src/main/resources/application-prod.yml
@@ -3,8 +3,8 @@ spring:
     name: unis-crm-backend
   servlet:
     multipart:
-      max-file-size: 20MB
-      max-request-size: 25MB
+      max-file-size: 500MB
+      max-request-size: 600MB
   datasource:
     url: jdbc:postgresql://192.168.124.202:5432/unis_crm
     username: postgres
@@ -81,3 +81,25 @@ unisbase:
       pre-sales-role-name: ${OMS_PRE_SALES_ROLE_NAME:售前}
       connect-timeout-seconds: ${OMS_CONNECT_TIMEOUT_SECONDS:5}
       read-timeout-seconds: ${OMS_READ_TIMEOUT_SECONDS:15}
+    work-report:
+      daily-report:
+        # 未命中下方任何角色配置时，是否默认要求日报使用 # 选择并关联对象。
+        required-by-default: false
+        # 明确要求使用 # 选择并关联对象的角色编码；命中后优先级高于 optional-role-codes。
+        required-role-codes: [crm_xs]
+        # 允许不使用 #，可直接填写文字日报的角色编码。
+        optional-role-codes: []
+        notify-users:
+          # 未命中下方任何角色配置时，是否默认显示“选择通知用户”按钮。
+          visible-by-default: true
+          # 明确显示“选择通知用户”按钮的角色编码。
+          visible-role-codes: []
+          # 明确隐藏“选择通知用户”按钮的角色编码；命中后优先级高于 visible-role-codes。
+          hidden-role-codes: []
+        attachments:
+          # 未命中下方任何角色配置时，是否默认显示日报附件功能。
+          visible-by-default: true
+          # 明确显示日报附件功能的角色编码。
+          visible-role-codes: []
+          # 明确隐藏日报附件功能的角色编码；命中后优先级高于 visible-role-codes。
+          hidden-role-codes: [crm_xs]
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index c911ad73..bc7f2817 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -8,8 +8,8 @@ spring:
     time-zone: Asia/Shanghai
   servlet:
     multipart:
-      max-file-size: 20MB
-      max-request-size: 25MB
+      max-file-size: 500MB
+      max-request-size: 600MB
   datasource:
     url: jdbc:postgresql://127.0.0.1:5432/unis_crm
     username: postgres
@@ -90,3 +90,25 @@ unisbase:
       pre-sales-role-name: ${OMS_PRE_SALES_ROLE_NAME:售前}
       connect-timeout-seconds: ${OMS_CONNECT_TIMEOUT_SECONDS:5}
       read-timeout-seconds: ${OMS_READ_TIMEOUT_SECONDS:15}
+    work-report:
+      daily-report:
+        # 未命中下方任何角色配置时，是否默认要求日报使用 # 选择并关联对象。
+        required-by-default: false
+        # 明确要求使用 # 选择并关联对象的角色编码；命中后优先级高于 optional-role-codes。
+        required-role-codes: [crm_xs]
+        # 允许不使用 #，可直接填写文字日报的角色编码。
+        optional-role-codes: []
+        notify-users:
+          # 未命中下方任何角色配置时，是否默认显示“选择通知用户”按钮。
+          visible-by-default: true
+          # 明确显示“选择通知用户”按钮的角色编码。
+          visible-role-codes: []
+          # 明确隐藏“选择通知用户”按钮的角色编码；命中后优先级高于 visible-role-codes。
+          hidden-role-codes: []
+        attachments:
+          # 未命中下方任何角色配置时，是否默认显示日报附件功能。
+          visible-by-default: true
+          # 明确显示日报附件功能的角色编码。
+          visible-role-codes: []
+          # 明确隐藏日报附件功能的角色编码；命中后优先级高于 visible-role-codes。
+          hidden-role-codes: [crm_xs]
diff --git a/backend/src/test/java/com/unis/crm/service/impl/WorkServiceImplTest.java b/backend/src/test/java/com/unis/crm/service/impl/WorkServiceImplTest.java
index 362aa487..8f2e79b4 100644
--- a/backend/src/test/java/com/unis/crm/service/impl/WorkServiceImplTest.java
+++ b/backend/src/test/java/com/unis/crm/service/impl/WorkServiceImplTest.java
@@ -8,6 +8,7 @@ import static org.mockito.Mockito.when;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.unis.crm.common.BusinessException;
+import com.unis.crm.config.WorkReportProperties;
 import com.unis.crm.dto.work.CreateWorkCheckInRequest;
 import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
 import com.unis.crm.dto.work.WorkDailyReportDTO;
@@ -57,9 +58,12 @@ class WorkServiceImplTest {
 
     private WorkServiceImpl workService;
 
+    private WorkReportProperties workReportProperties;
+
     @BeforeEach
     void setUp() {
-        workService = new WorkServiceImpl(workMapper, opportunityMapper, profileMapper, reportReminderService, new ObjectMapper(), tenantProvider, "build/test-uploads", "");
+        workReportProperties = new WorkReportProperties();
+        workService = new WorkServiceImpl(workMapper, opportunityMapper, profileMapper, reportReminderService, workReportProperties, new ObjectMapper(), tenantProvider, "build/test-uploads", "");
     }
 
     @Test
@@ -150,8 +154,65 @@ class WorkServiceImplTest {
                 org.mockito.ArgumentMatchers.any());
     }
 
+    @Test
+    void listReportMessageUsers_shouldRejectHiddenRole() {
+        workReportProperties.getDailyReport().getNotifyUsers().setHiddenRoleCodes(List.of("sales"));
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> workService.listReportMessageUsers(17L, ""));
+
+        assertEquals("当前角色不可使用日报通知用户功能", exception.getMessage());
+        verify(workMapper, never()).selectReportMessageUsers(any(), any(), eq(50));
+    }
+
+    @Test
+    void saveDailyReport_shouldRejectEmptyContentByDefault() {
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> workService.saveDailyReport(17L, new CreateWorkDailyReportRequest()));
+
+        assertEquals("请至少填写一条今日工作内容", exception.getMessage());
+        verify(workMapper, never()).insertDailyReport(
+                org.mockito.ArgumentMatchers.anyLong(),
+                org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any());
+    }
+
+    @Test
+    void saveDailyReport_shouldRejectManualTextWhenObjectSelectionRequiredByDefault() {
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
+        WorkReportLineItemRequest lineItem = new WorkReportLineItemRequest();
+        lineItem.setWorkDate("2026-04-28");
+        lineItem.setBizType("sales");
+        lineItem.setBizId(0L);
+        lineItem.setEditorText("整理客户资料，完成重点项目复盘");
+        lineItem.setContent("整理客户资料，完成重点项目复盘");
+        request.setLineItems(List.of(lineItem));
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> workService.saveDailyReport(17L, request));
+
+        assertEquals("请使用 # 选择日报关联对象", exception.getMessage());
+        verify(workMapper, never()).insertDailyReport(
+                org.mockito.ArgumentMatchers.anyLong(),
+                org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any());
+    }
+
     @Test
     void saveDailyReport_shouldAllowManualTextWithoutLinkedObject() {
+        workReportProperties.getDailyReport().setOptionalRoleCodes(List.of("sales_assistant"));
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales_assistant"));
+
         CreateWorkDailyReportRequest request = new CreateWorkDailyReportRequest();
         WorkReportLineItemRequest lineItem = new WorkReportLineItemRequest();
         lineItem.setWorkDate("2026-04-28");
@@ -203,6 +264,59 @@ class WorkServiceImplTest {
         assertEquals("当前角色仅可查看打卡历史记录", exception.getMessage());
     }
 
+    @Test
+    void uploadReportAttachment_shouldAllowLargeZipWithinLimit() {
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        MockMultipartFile file = new MockMultipartFile(
+                "file",
+                "project-pack.zip",
+                "application/zip",
+                new byte[] {1, 2, 3, 4});
+
+        WorkReportAttachmentDTO attachment = workService.uploadReportAttachment(17L, file);
+
+        assertEquals("project-pack.zip", attachment.getName());
+        assertEquals("application/zip", attachment.getContentType());
+        assertEquals(4L, attachment.getSize());
+    }
+
+    @Test
+    void uploadReportAttachment_shouldRejectHiddenRole() {
+        workReportProperties.getDailyReport().getAttachments().setHiddenRoleCodes(List.of("sales"));
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> workService.uploadReportAttachment(
+                        17L,
+                        new MockMultipartFile("file", "project-pack.zip", "application/zip", new byte[] {1, 2, 3})));
+
+        assertEquals("当前角色不可使用日报附件功能", exception.getMessage());
+    }
+
+    @Test
+    void uploadReportAttachment_shouldRejectOverLimitFiles() {
+        when(profileMapper.selectUserRoleCodes(17L)).thenReturn(List.of("sales"));
+
+        MockMultipartFile file = new MockMultipartFile(
+                "file",
+                "project-pack.zip",
+                "application/zip",
+                new byte[] {1}) {
+            @Override
+            public long getSize() {
+                return 500L * 1024 * 1024 + 1;
+            }
+        };
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> workService.uploadReportAttachment(17L, file));
+
+        assertEquals("单个附件不能超过500MB", exception.getMessage());
+    }
+
     @Test
     void exportCheckIns_shouldCleanPhotoMetadata() {
         WorkCheckInExportDTO row = new WorkCheckInExportDTO();
diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts
index 68a41185..98a3aa3d 100644
--- a/frontend/src/lib/auth.ts
+++ b/frontend/src/lib/auth.ts
@@ -318,6 +318,10 @@ export interface WorkReportAttachment {
   size?: number;
 }
 
+export interface UploadReportAttachmentOptions {
+  onProgress?: (progress: number) => void;
+}
+
 export interface WorkReportToUser {
   userId: number;
   name: string;
@@ -368,6 +372,12 @@ export interface WorkOverview {
   history?: WorkHistoryItem[];
 }
 
+export interface WorkDailyReportRequirement {
+  objectSelectionRequired?: boolean;
+  notifyUsersVisible?: boolean;
+  attachmentsVisible?: boolean;
+}
+
 export interface WorkHistoryPage {
   items?: WorkHistoryItem[];
   hasMore?: boolean;
@@ -1311,6 +1321,10 @@ export async function getWorkOverview() {
   return request<WorkOverview>("/api/work/overview", undefined, true);
 }
 
+export async function getWorkDailyReportRequirement() {
+  return request<WorkDailyReportRequirement>("/api/work/daily-report-requirement", undefined, true);
+}
+
 export async function listWorkReportMessageUsers(keyword?: string) {
   const params = new URLSearchParams();
   const normalizedKeyword = keyword?.trim() || "";
@@ -1396,13 +1410,61 @@ export async function uploadWorkCheckInPhoto(file: File) {
   }, true);
 }
 
-export async function uploadWorkReportAttachment(file: File) {
-  const formData = new FormData();
-  formData.append("file", file);
-  return request<WorkReportAttachment>("/api/work/report-attachments", {
-    method: "POST",
-    body: formData,
-  }, true);
+export async function uploadWorkReportAttachment(file: File, options?: UploadReportAttachmentOptions) {
+  const token = localStorage.getItem("accessToken");
+  const userId = getStoredUserId();
+  const activeTenantId = localStorage.getItem("activeTenantId");
+
+  return new Promise<WorkReportAttachment>((resolve, reject) => {
+    const xhr = new XMLHttpRequest();
+    xhr.open("POST", "/api/work/report-attachments");
+    if (token) {
+      xhr.setRequestHeader("Authorization", `Bearer ${token}`);
+    }
+    if (userId !== undefined) {
+      xhr.setRequestHeader("X-User-Id", String(userId));
+    }
+    if (activeTenantId?.trim()) {
+      xhr.setRequestHeader("X-Tenant-Id", activeTenantId.trim());
+    }
+
+    xhr.upload.onprogress = (event) => {
+      if (!event.lengthComputable || !options?.onProgress) {
+        return;
+      }
+      options.onProgress(Math.min(100, Math.round((event.loaded / event.total) * 100)));
+    };
+
+    xhr.onerror = () => reject(new Error("附件上传失败，请稍后重试"));
+    xhr.onabort = () => reject(new Error("附件上传已取消"));
+    xhr.onload = () => {
+      const rawText = xhr.responseText || "";
+      const body = tryParseApiBody<WorkReportAttachment>(rawText, xhr.getResponseHeader("content-type"));
+      const errorMessage = buildApiErrorMessage(rawText, xhr.status, body);
+
+      if (xhr.status === 401 || (xhr.status >= 300 && xhr.status < 400)) {
+        handleUnauthorizedResponse();
+        reject(new Error("登录已失效，请重新登录"));
+        return;
+      }
+
+      if (xhr.status < 200 || xhr.status >= 300) {
+        reject(new Error(errorMessage));
+        return;
+      }
+
+      if (!body || !isSuccessCode(body.code)) {
+        reject(new Error(errorMessage));
+        return;
+      }
+
+      resolve(body.data);
+    };
+
+    const formData = new FormData();
+    formData.append("file", file);
+    xhr.send(formData);
+  });
 }
 
 export async function saveWorkDailyReport(payload: CreateWorkDailyReportPayload) {
diff --git a/frontend/src/pages/Work.tsx b/frontend/src/pages/Work.tsx
index e5e0ec30..85d1c0f2 100644
--- a/frontend/src/pages/Work.tsx
+++ b/frontend/src/pages/Work.tsx
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, ty
 import { format } from "date-fns";
 import { zhCN } from "date-fns/locale";
 import { motion } from "motion/react";
-import { ArrowUp, Camera, CheckCircle2, ChevronDown, Download, FileText, Hash, ListTodo, MapPin, NotebookPen, Paperclip, Plus, RefreshCw, Search, Send, Trash2, UserRoundPlus, X } from "lucide-react";
+import { ArrowUp, Camera, CheckCircle2, ChevronDown, Download, FileText, Hash, ListTodo, MapPin, NotebookPen, Paperclip, Plus, RefreshCw, RotateCcw, Search, Send, Trash2, UserRoundPlus, X } from "lucide-react";
 import { flushSync } from "react-dom";
 import { Link, Navigate, useLocation, useNavigate } from "react-router-dom";
 import {
@@ -20,6 +20,7 @@ import {
   getProfileOverview,
   getWorkCheckInExportData,
   getWorkDailyReportExportData,
+  getWorkDailyReportRequirement,
   getWorkReportHistoryItem,
   refreshCurrentUser,
   getWorkHistory,
@@ -80,6 +81,7 @@ const LEGACY_NEXT_PLAN_LABEL = "后续规划";
 const OPPORTUNITY_NEXT_PLAN_LABEL = "下一步销售计划";
 const OPPORTUNITY_STAGE_LABEL = "项目阶段";
 const WORK_DETAIL_NEXT_PLAN_HEADER = "后续规划 / 下一步销售计划";
+const REPORT_ATTACHMENT_MAX_SIZE_BYTES = 500 * 1024 * 1024;
 
 const reportFieldLabels = {
   sales: ["沟通内容", LEGACY_NEXT_PLAN_LABEL],
@@ -797,6 +799,9 @@ export default function Work() {
   const [submittingReport, setSubmittingReport] = useState(false);
   const [uploadingPhoto, setUploadingPhoto] = useState(false);
   const [uploadingReportAttachmentIndex, setUploadingReportAttachmentIndex] = useState<number | null>(null);
+  const [uploadingReportAttachmentProgress, setUploadingReportAttachmentProgress] = useState<Record<number, number>>({});
+  const [reportAttachmentUploadErrors, setReportAttachmentUploadErrors] = useState<Record<number, string>>({});
+  const reportAttachmentFileRef = useRef<Record<number, File | null>>({});
   const [mobilePanel, setMobilePanel] = useState<"entry" | "history">("entry");
   const [locationHint, setLocationHint] = useState("");
   const [locationAdjustOpen, setLocationAdjustOpen] = useState(false);
@@ -823,6 +828,10 @@ export default function Work() {
   const [historyPresenterPickerOpen, setHistoryPresenterPickerOpen] = useState(false);
   const [expandedHistoryGroupKey, setExpandedHistoryGroupKey] = useState<string | null>(null);
   const [reportStatus, setReportStatus] = useState<string>();
+  const [dailyReportFeatureLoaded, setDailyReportFeatureLoaded] = useState(false);
+  const [dailyReportObjectSelectionRequired, setDailyReportObjectSelectionRequired] = useState(true);
+  const [dailyReportNotifyUsersVisible, setDailyReportNotifyUsersVisible] = useState(false);
+  const [dailyReportAttachmentsVisible, setDailyReportAttachmentsVisible] = useState(false);
   const [currentUser, setCurrentUser] = useState<UserProfile | null>(() => readStoredUserProfile());
   const [profileOverview, setProfileOverview] = useState<ProfileOverview | null>(null);
   const [checkInPhotoUrls, setCheckInPhotoUrls] = useState<string[]>([]);
@@ -1196,19 +1205,31 @@ export default function Work() {
 
     async function loadUserContext() {
       try {
-        const [userData, overviewData] = await Promise.all([
+        const [userData, overviewData, requirementData] = await Promise.all([
           ((currentUser?.roleCodes?.length ?? 0) > 0 ? getCurrentUser() : refreshCurrentUser()).catch(() => null),
           getProfileOverview().catch(() => null),
+          getWorkDailyReportRequirement().catch(() => null),
         ]);
         if (cancelled) {
           return;
         }
         setCurrentUser(userData);
         setProfileOverview(overviewData);
+        if (requirementData && typeof requirementData.objectSelectionRequired === "boolean") {
+          setDailyReportObjectSelectionRequired(requirementData.objectSelectionRequired);
+        }
+        if (requirementData && typeof requirementData.notifyUsersVisible === "boolean") {
+          setDailyReportNotifyUsersVisible(requirementData.notifyUsersVisible);
+        }
+        if (requirementData && typeof requirementData.attachmentsVisible === "boolean") {
+          setDailyReportAttachmentsVisible(requirementData.attachmentsVisible);
+        }
+        setDailyReportFeatureLoaded(true);
       } catch {
         if (!cancelled) {
           setCurrentUser(null);
           setProfileOverview(null);
+          setDailyReportFeatureLoaded(true);
         }
       }
     }
@@ -1925,12 +1946,31 @@ export default function Work() {
       return;
     }
 
+    if (file.size > REPORT_ATTACHMENT_MAX_SIZE_BYTES) {
+      const message = `单个附件不能超过${formatAttachmentSize(REPORT_ATTACHMENT_MAX_SIZE_BYTES)}`;
+      setReportError(message);
+      setReportAttachmentUploadErrors((current) => ({ ...current, [index]: message }));
+      event.target.value = "";
+      return;
+    }
+
+    reportAttachmentFileRef.current[index] = file;
     setReportError("");
     setReportSuccess("");
     setUploadingReportAttachmentIndex(index);
+    setReportAttachmentUploadErrors((current) => {
+      const next = { ...current };
+      delete next[index];
+      return next;
+    });
+    setUploadingReportAttachmentProgress((current) => ({ ...current, [index]: 0 }));
 
     try {
-      const attachment = await uploadWorkReportAttachment(file);
+      const attachment = await uploadWorkReportAttachment(file, {
+        onProgress: (progress) => {
+          setUploadingReportAttachmentProgress((current) => ({ ...current, [index]: progress }));
+        },
+      });
       setReportForm((current) => ({
         ...current,
         lineItems: current.lineItems.map((item, itemIndex) => {
@@ -1947,10 +1987,18 @@ export default function Work() {
           };
         }),
       }));
+      reportAttachmentFileRef.current[index] = null;
     } catch (error) {
-      setReportError(error instanceof Error ? error.message : "附件上传失败");
+      const message = error instanceof Error ? error.message : "附件上传失败";
+      setReportError(message);
+      setReportAttachmentUploadErrors((current) => ({ ...current, [index]: message }));
     } finally {
       setUploadingReportAttachmentIndex(null);
+      setUploadingReportAttachmentProgress((current) => {
+        const next = { ...current };
+        delete next[index];
+        return next;
+      });
       event.target.value = "";
     }
   };
@@ -1966,6 +2014,18 @@ export default function Work() {
     }));
   };
 
+  const handleRetryReportAttachment = (index: number) => {
+    const file = reportAttachmentFileRef.current[index];
+    if (!file) {
+      setReportError("请重新选择附件后再试");
+      return;
+    }
+    const fakeEvent = {
+      target: { files: [file], value: "" },
+    } as unknown as ChangeEvent<HTMLInputElement>;
+    void handleReportAttachmentChange(index, fakeEvent);
+  };
+
   const handlePlanItemChange = (index: number, value: string) => {
     setReportForm((current) => ({
       ...current,
@@ -2056,11 +2116,16 @@ export default function Work() {
     setSubmittingReport(true);
 
     try {
+      if (!dailyReportFeatureLoaded) {
+        throw new Error("日报功能配置加载中，请稍后重试");
+      }
       if (isOnlySeeRole) {
         throw new Error("当前角色仅可查看日报历史记录");
       }
       const normalizedLineItems = reportForm.lineItems.map((item) => (
         normalizeReportLineItem(item, currentWorkDate, reportOpportunityStageOptions)
+      )).map((item) => (
+        normalizeLineFeatureVisibility(item, dailyReportNotifyUsersVisible, dailyReportAttachmentsVisible)
       ));
       const normalizedPlanItems = reportForm.planItems
         .map((item) => ({ content: item.content.trim() }))
@@ -2069,7 +2134,7 @@ export default function Work() {
       if (!normalizedLineItems.length) {
         throw new Error("请至少填写一条今日工作内容");
       }
-      validateReportLineItems(normalizedLineItems, reportOpportunityStageOptions);
+      validateReportLineItems(normalizedLineItems, reportOpportunityStageOptions, dailyReportObjectSelectionRequired);
 
       await saveWorkDailyReport({
         workContent: buildReportSummary(normalizedLineItems),
@@ -2191,7 +2256,13 @@ export default function Work() {
                 onReportLineChange={handleReportLineChange}
                 onReportLineStageChange={handleReportLineStageChange}
                 uploadingReportAttachmentIndex={uploadingReportAttachmentIndex}
+                uploadingReportAttachmentProgress={uploadingReportAttachmentProgress}
+                reportAttachmentUploadErrors={reportAttachmentUploadErrors}
+                dailyReportFeatureLoaded={dailyReportFeatureLoaded}
+                notifyUsersVisible={dailyReportNotifyUsersVisible}
+                attachmentsVisible={dailyReportAttachmentsVisible}
                 onReportAttachmentChange={(index, event) => void handleReportAttachmentChange(index, event)}
+                onRetryReportAttachment={handleRetryReportAttachment}
                 onRemoveReportAttachment={handleRemoveReportAttachment}
                 onPreviewAttachment={setPreviewAttachment}
                 onAddPlanItem={handleAddPlanItem}
@@ -3734,18 +3805,52 @@ function ReportAttachmentList({
   attachments,
   onPreview,
   onRemove,
+  uploadProgress,
+  uploadError,
+  onRetryUpload,
 }: {
   attachments?: WorkReportAttachment[];
   onPreview: (attachment: WorkReportAttachment) => void;
   onRemove?: (attachment: WorkReportAttachment) => void;
+  uploadProgress?: number;
+  uploadError?: string;
+  onRetryUpload?: () => void;
 }) {
-  if (!attachments?.length) {
+  if (!attachments?.length && typeof uploadProgress !== "number" && !uploadError) {
     return null;
   }
 
   return (
     <div className="flex flex-wrap gap-2">
-      {attachments.map((attachment) => (
+      {typeof uploadProgress === "number" ? (
+        <div className="inline-flex items-center gap-2 rounded-xl border border-violet-200 bg-violet-50 px-2.5 py-1.5 text-xs text-violet-700 shadow-sm dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-200">
+          <RefreshCw className="h-3.5 w-3.5 animate-spin" />
+          <span>上传中 {uploadProgress}%</span>
+          <div className="h-1.5 w-20 overflow-hidden rounded-full bg-violet-100 dark:bg-violet-500/20">
+            <div
+              className="h-full rounded-full bg-violet-600 transition-[width] dark:bg-violet-300"
+              style={{ width: `${Math.max(3, uploadProgress)}%` }}
+            />
+          </div>
+        </div>
+      ) : null}
+      {uploadError ? (
+        <div className="inline-flex max-w-full items-center gap-2 rounded-xl border border-rose-200 bg-rose-50 px-2.5 py-1.5 text-xs text-rose-700 shadow-sm dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200">
+          <span className="max-w-[180px] truncate">{uploadError}</span>
+          {onRetryUpload ? (
+            <button
+              type="button"
+              onClick={onRetryUpload}
+              className="inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 font-medium transition-colors hover:bg-rose-100 dark:hover:bg-rose-500/20"
+              title="重试上传"
+            >
+              <RotateCcw className="h-3 w-3" />
+              重试
+            </button>
+          ) : null}
+        </div>
+      ) : null}
+      {(attachments ?? []).map((attachment) => (
         <div
           key={attachment.url}
           className="inline-flex max-w-full items-center gap-2 rounded-xl border border-slate-200 bg-white px-2.5 py-1.5 text-xs text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-300"
@@ -4009,7 +4114,13 @@ function ReportPanel({
   onReportLineChange,
   onReportLineStageChange,
   uploadingReportAttachmentIndex,
+  uploadingReportAttachmentProgress,
+  reportAttachmentUploadErrors,
+  dailyReportFeatureLoaded,
+  notifyUsersVisible,
+  attachmentsVisible,
   onReportAttachmentChange,
+  onRetryReportAttachment,
   onRemoveReportAttachment,
   onPreviewAttachment,
   onAddPlanItem,
@@ -4035,7 +4146,13 @@ function ReportPanel({
   onReportLineChange: (index: number, value: string) => void;
   onReportLineStageChange: (index: number, value: string) => void;
   uploadingReportAttachmentIndex: number | null;
+  uploadingReportAttachmentProgress: Record<number, number>;
+  reportAttachmentUploadErrors: Record<number, string>;
+  dailyReportFeatureLoaded: boolean;
+  notifyUsersVisible: boolean;
+  attachmentsVisible: boolean;
   onReportAttachmentChange: (index: number, event: ChangeEvent<HTMLInputElement>) => void;
+  onRetryReportAttachment: (index: number) => void;
   onRemoveReportAttachment: (lineIndex: number, attachmentUrl: string) => void;
   onPreviewAttachment: (attachment: WorkReportAttachment) => void;
   onAddPlanItem: () => void;
@@ -4215,6 +4332,8 @@ function ReportPanel({
               : "";
             const attachments = item.attachments ?? [];
             const isUploadingAttachment = uploadingReportAttachmentIndex === index;
+            const attachmentProgress = uploadingReportAttachmentProgress[index] ?? 0;
+            const attachmentUploadError = reportAttachmentUploadErrors[index];
             return (
               <div key={`report-line-${index}`} className="relative rounded-2xl border border-slate-100/90 bg-slate-50/30 p-3 dark:border-slate-800/70 dark:bg-slate-900/20">
                 <span
@@ -4297,11 +4416,16 @@ function ReportPanel({
                       placeholder="输入工作内容，或输入 # 选择对象生成字段。"
                       className="crm-input-box crm-input-text min-h-[88px] w-full resize-none overflow-hidden rounded-2xl border border-slate-200 bg-white leading-7 text-slate-900 outline-none transition-colors focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/60 dark:text-white"
                     />
-                    <ReportAttachmentList
-                      attachments={attachments}
-                      onPreview={onPreviewAttachment}
-                      onRemove={(attachment) => onRemoveReportAttachment(index, attachment.url)}
-                    />
+                    {attachmentsVisible ? (
+                      <ReportAttachmentList
+                        attachments={attachments}
+                        onPreview={onPreviewAttachment}
+                        onRemove={(attachment) => onRemoveReportAttachment(index, attachment.url)}
+                        uploadProgress={isUploadingAttachment ? attachmentProgress : undefined}
+                        uploadError={attachmentUploadError}
+                        onRetryUpload={attachmentUploadError ? () => onRetryReportAttachment(index) : undefined}
+                      />
+                    ) : null}
                   </div>
                 ) : (
                   <div className="min-w-0 flex-1 space-y-2">
@@ -4341,22 +4465,29 @@ function ReportPanel({
                         </div>
                       </div>
                     </button>
-                    <ReportAttachmentList
-                      attachments={attachments}
-                      onPreview={onPreviewAttachment}
-                      onRemove={(attachment) => onRemoveReportAttachment(index, attachment.url)}
-                    />
+                    {attachmentsVisible ? (
+                      <ReportAttachmentList
+                        attachments={attachments}
+                        onPreview={onPreviewAttachment}
+                        onRemove={(attachment) => onRemoveReportAttachment(index, attachment.url)}
+                        uploadProgress={isUploadingAttachment ? attachmentProgress : undefined}
+                        uploadError={attachmentUploadError}
+                        onRetryUpload={attachmentUploadError ? () => onRetryReportAttachment(index) : undefined}
+                      />
+                    ) : null}
                   </div>
                 )}
                 <div className="flex w-full shrink-0 items-center justify-end gap-2 sm:w-auto sm:self-center lg:grid lg:w-[88px] lg:grid-cols-2 lg:self-start">
-                  <input
-                    ref={(element) => {
-                      reportAttachmentInputRefs.current[index] = element;
-                    }}
-                    type="file"
-                    className="hidden"
-                    onChange={(event) => onReportAttachmentChange(index, event)}
-                  />
+                  {attachmentsVisible ? (
+                    <input
+                      ref={(element) => {
+                        reportAttachmentInputRefs.current[index] = element;
+                      }}
+                      type="file"
+                      className="hidden"
+                      onChange={(event) => onReportAttachmentChange(index, event)}
+                    />
+                  ) : null}
                   <button
                     type="button"
                     onClick={() => onOpenObjectPicker(index, item.bizType || "sales")}
@@ -4366,25 +4497,36 @@ function ReportPanel({
                   >
                     <Hash className="h-4.5 w-4.5" />
                   </button>
-                  <button
-                    type="button"
-                    onClick={() => onOpenUserPicker(index)}
-                    className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-violet-200 bg-violet-50 text-violet-600 shadow-sm transition-colors hover:bg-violet-100 hover:text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-300 dark:hover:bg-violet-500/20"
-                    title="选择通知用户"
-                    aria-label="选择通知用户"
-                  >
-                    <UserRoundPlus className="h-4.5 w-4.5" />
-                  </button>
-                  <button
-                    type="button"
-                    onClick={() => reportAttachmentInputRefs.current[index]?.click()}
-                    disabled={isUploadingAttachment}
-                    className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm transition-colors hover:border-violet-200 hover:bg-violet-50 hover:text-violet-600 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-300 dark:hover:border-violet-500/30 dark:hover:bg-violet-500/10 dark:hover:text-violet-300"
-                    title={isUploadingAttachment ? "附件上传中" : "上传附件"}
-                    aria-label={isUploadingAttachment ? "附件上传中" : "上传附件"}
-                  >
-                    <Paperclip className="h-4.5 w-4.5" />
-                  </button>
+                  {notifyUsersVisible ? (
+                    <button
+                      type="button"
+                      onClick={() => onOpenUserPicker(index)}
+                      className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-violet-200 bg-violet-50 text-violet-600 shadow-sm transition-colors hover:bg-violet-100 hover:text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-300 dark:hover:bg-violet-500/20"
+                      title="选择通知用户"
+                      aria-label="选择通知用户"
+                    >
+                      <UserRoundPlus className="h-4.5 w-4.5" />
+                    </button>
+                  ) : null}
+                  {attachmentsVisible ? (
+                    <button
+                      type="button"
+                      onClick={() => reportAttachmentInputRefs.current[index]?.click()}
+                      disabled={isUploadingAttachment}
+                      className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm transition-colors hover:border-violet-200 hover:bg-violet-50 hover:text-violet-600 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-300 dark:hover:border-violet-500/30 dark:hover:bg-violet-500/10 dark:hover:text-violet-300"
+                      title={isUploadingAttachment ? `附件上传中 ${attachmentProgress}%` : "上传附件"}
+                      aria-label={isUploadingAttachment ? `附件上传中 ${attachmentProgress}%` : "上传附件"}
+                    >
+                      {isUploadingAttachment ? (
+                        <div className="flex flex-col items-center gap-0.5">
+                          <RefreshCw className="h-4.5 w-4.5 animate-spin" />
+                          <span className="text-[10px] font-medium leading-none text-slate-400">{attachmentProgress}%</span>
+                        </div>
+                      ) : (
+                        <Paperclip className="h-4.5 w-4.5" />
+                      )}
+                    </button>
+                  ) : null}
                   <button
                     type="button"
                     onClick={() => onRemoveReportLine(index)}
@@ -4475,7 +4617,7 @@ function ReportPanel({
       <button
         type="button"
         onClick={onSubmit}
-        disabled={submittingReport || loading}
+        disabled={submittingReport || loading || !dailyReportFeatureLoaded}
         className={cn(
           "crm-btn crm-btn-primary mt-6 flex w-full items-center justify-center gap-2 disabled:cursor-not-allowed disabled:opacity-60",
           disableMobileMotion ? "active:scale-100" : "active:scale-[0.98]",
@@ -5335,9 +5477,35 @@ function normalizeReportLineItem(
   };
 }
 
-function validateReportLineItems(lineItems: WorkReportLineItem[], opportunityStageOptions: OpportunityDictOption[]) {
+function normalizeLineFeatureVisibility(
+  item: WorkReportLineItem,
+  notifyUsersVisible: boolean,
+  attachmentsVisible: boolean,
+) {
+  if (notifyUsersVisible && attachmentsVisible) {
+    return item;
+  }
+  const nextItem: WorkReportLineItem = { ...item };
+  if (!notifyUsersVisible) {
+    nextItem.toUsers = [];
+    nextItem.editorText = stripReportToLines(nextItem.editorText);
+  }
+  if (!attachmentsVisible) {
+    nextItem.attachments = [];
+  }
+  return nextItem;
+}
+
+function validateReportLineItems(
+  lineItems: WorkReportLineItem[],
+  opportunityStageOptions: OpportunityDictOption[],
+  objectSelectionRequired: boolean,
+) {
   for (const item of lineItems) {
     if (!item.bizId || !item.bizName) {
+      if (objectSelectionRequired) {
+        throw new Error("请使用 # 选择日报关联对象");
+      }
       if (!item.editorText?.trim() && !item.content?.trim()) {
         throw new Error("请填写今日工作内容");
       }
