diff --git a/backend/build/test-uploads/work-report-attachments/17-369a7e5f9e5b4fd58d5519d13c68ad0e.zip b/backend/build/test-uploads/work-report-attachments/17-369a7e5f9e5b4fd58d5519d13c68ad0e.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-369a7e5f9e5b4fd58d5519d13c68ad0e.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/build/test-uploads/work-report-attachments/17-549f73368b3c44a1a1142fba5c00edc4.zip b/backend/build/test-uploads/work-report-attachments/17-549f73368b3c44a1a1142fba5c00edc4.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-549f73368b3c44a1a1142fba5c00edc4.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/build/test-uploads/work-report-attachments/17-be54a89fd93d4385b686741614343c2d.zip b/backend/build/test-uploads/work-report-attachments/17-be54a89fd93d4385b686741614343c2d.zip
new file mode 100644
index 00000000..82090ee2
--- /dev/null
+++ b/backend/build/test-uploads/work-report-attachments/17-be54a89fd93d4385b686741614343c2d.zip
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java b/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java
index 50c6a047..f5560f1c 100644
--- a/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java
+++ b/backend/src/main/java/com/unis/crm/common/CrmGlobalExceptionHandler.java
@@ -1,9 +1,13 @@
 package com.unis.crm.common;
 
 import jakarta.servlet.http.HttpServletRequest;
+import java.sql.SQLException;
 import java.time.OffsetDateTime;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -21,6 +25,7 @@ import org.springframework.validation.method.ParameterValidationResult;
 @RestControllerAdvice
 public class CrmGlobalExceptionHandler {
 
+    private static final Logger log = LoggerFactory.getLogger(CrmGlobalExceptionHandler.class);
     private static final String CURRENT_USER_HEADER = "X-User-Id";
     private static final String UNAUTHORIZED_MESSAGE = "登录已失效，请重新登录";
 
@@ -95,16 +100,63 @@ public class CrmGlobalExceptionHandler {
         return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build();
     }
 
+    @ExceptionHandler(DataIntegrityViolationException.class)
+    public ResponseEntity<Map<String, Object>> handleDataIntegrityViolation(
+            DataIntegrityViolationException ex,
+            HttpServletRequest request) {
+        log.warn("Database constraint violation on {}", request.getRequestURI(), ex);
+        return compatibleErrorResponse(HttpStatus.CONFLICT, resolveConstraintMessage(ex), request.getRequestURI());
+    }
+
     @ExceptionHandler(Exception.class)
-    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
-    public Map<String, Object> handleUnexpectedException(Exception ex, HttpServletRequest request) {
+    public ResponseEntity<Map<String, Object>> handleUnexpectedException(Exception ex, HttpServletRequest request) {
+        log.error("Unexpected request failure on {}", request.getRequestURI(), ex);
+        return compatibleErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "系统内部错误，请稍后重试", request.getRequestURI());
+    }
+
+    private String resolveConstraintMessage(Throwable throwable) {
+        SQLException sqlException = findCause(throwable, SQLException.class);
+        String databaseMessage = sqlException == null ? "" : String.valueOf(sqlException.getMessage());
+        if (databaseMessage.contains("uk_crm_customer_code")) {
+            return "客户编码生成冲突，请重试";
+        }
+        if (databaseMessage.contains("uk_crm_opportunity_code")) {
+            return "商机编码生成冲突，请重试";
+        }
+        if (databaseMessage.contains("fk_crm_opportunity_channel_expansion")) {
+            return "所选渠道不存在或已失效，请重新选择";
+        }
+        if (databaseMessage.contains("fk_crm_opportunity_sales_expansion")) {
+            return "所选销售拓展不存在或已失效，请重新选择";
+        }
+        return "数据保存失败，请检查输入后重试";
+    }
+
+    private <T extends Throwable> T findCause(Throwable throwable, Class<T> causeType) {
+        Throwable current = throwable;
+        while (current != null) {
+            if (causeType.isInstance(current)) {
+                return causeType.cast(current);
+            }
+            current = current.getCause();
+        }
+        return null;
+    }
+
+    private ResponseEntity<Map<String, Object>> compatibleErrorResponse(
+            HttpStatus status,
+            String message,
+            String path) {
         Map<String, Object> body = new LinkedHashMap<>();
+        body.put("code", "-1");
+        body.put("msg", message);
+        body.put("data", null);
         body.put("timestamp", OffsetDateTime.now().toString());
-        body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
-        body.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
-        body.put("message", ex.getMessage());
-        body.put("path", request.getRequestURI());
-        return body;
+        body.put("status", status.value());
+        body.put("error", status.getReasonPhrase());
+        body.put("message", message);
+        body.put("path", path);
+        return ResponseEntity.status(status).body(body);
     }
 
     private ResponseEntity<ApiResponse<Object>> errorResponse(HttpStatus status, String message) {
diff --git a/backend/src/main/java/com/unis/crm/common/CrmIdJacksonConfig.java b/backend/src/main/java/com/unis/crm/common/CrmIdJacksonConfig.java
new file mode 100644
index 00000000..632757b4
--- /dev/null
+++ b/backend/src/main/java/com/unis/crm/common/CrmIdJacksonConfig.java
@@ -0,0 +1,105 @@
+package com.unis.crm.common;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
+import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
+import java.io.IOException;
+import java.util.List;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class CrmIdJacksonConfig {
+
+    private static final long JS_MAX_SAFE_INTEGER = 9_007_199_254_740_991L;
+
+    public static Object toJsonCompatibleValue(Long id) {
+        if (id == null) {
+            return null;
+        }
+        return isJavaScriptSafeInteger(id) ? id : id.toString();
+    }
+
+    @Bean
+    public SimpleModule crmIdSerializationModule() {
+        SimpleModule module = new SimpleModule("crm-id-serialization");
+        module.setSerializerModifier(new CrmIdBeanSerializerModifier());
+        return module;
+    }
+
+    private static final class CrmIdBeanSerializerModifier extends BeanSerializerModifier {
+
+        @Override
+        public List<BeanPropertyWriter> changeProperties(
+                SerializationConfig config,
+                BeanDescription beanDesc,
+                List<BeanPropertyWriter> beanProperties) {
+            for (BeanPropertyWriter property : beanProperties) {
+                if (!isIdProperty(property.getName()) || property.getSerializer() != null) {
+                    continue;
+                }
+                JavaType type = property.getType();
+                if (isLong(type)) {
+                    property.assignSerializer(new CompatibleLongIdSerializer());
+                } else if (type.isCollectionLikeType() && isLong(type.getContentType())) {
+                    property.assignSerializer(new LongIdCollectionSerializer());
+                }
+            }
+            return beanProperties;
+        }
+
+        private boolean isIdProperty(String name) {
+            return "id".equalsIgnoreCase(name) || name.endsWith("Id") || name.endsWith("Ids");
+        }
+
+        private boolean isLong(JavaType type) {
+            return type != null && (type.hasRawClass(Long.class) || type.hasRawClass(long.class));
+        }
+    }
+
+    private static final class CompatibleLongIdSerializer extends JsonSerializer<Object> {
+
+        @Override
+        public void serialize(Object value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
+            long id = ((Number) value).longValue();
+            if (isJavaScriptSafeInteger(id)) {
+                generator.writeNumber(id);
+            } else {
+                generator.writeString(Long.toString(id));
+            }
+        }
+    }
+
+    private static final class LongIdCollectionSerializer extends JsonSerializer<Object> {
+
+        @Override
+        public void serialize(Object value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
+            generator.writeStartArray();
+            if (value instanceof Iterable<?> items) {
+                for (Object item : items) {
+                    if (item == null) {
+                        generator.writeNull();
+                    } else {
+                        long id = ((Number) item).longValue();
+                        if (isJavaScriptSafeInteger(id)) {
+                            generator.writeNumber(id);
+                        } else {
+                            generator.writeString(Long.toString(id));
+                        }
+                    }
+                }
+            }
+            generator.writeEndArray();
+        }
+    }
+
+    private static boolean isJavaScriptSafeInteger(long value) {
+        return value >= -JS_MAX_SAFE_INTEGER && value <= JS_MAX_SAFE_INTEGER;
+    }
+}
diff --git a/backend/src/main/java/com/unis/crm/common/OpportunitySchemaInitializer.java b/backend/src/main/java/com/unis/crm/common/OpportunitySchemaInitializer.java
index 8fd6a622..4547a46b 100644
--- a/backend/src/main/java/com/unis/crm/common/OpportunitySchemaInitializer.java
+++ b/backend/src/main/java/com/unis/crm/common/OpportunitySchemaInitializer.java
@@ -16,6 +16,8 @@ import org.springframework.stereotype.Component;
 public class OpportunitySchemaInitializer implements ApplicationRunner {
 
     private static final Logger log = LoggerFactory.getLogger(OpportunitySchemaInitializer.class);
+    private static final String CUSTOMER_CODE_SEQUENCE = "crm_customer_code_seq";
+    private static final String OPPORTUNITY_CODE_SEQUENCE = "crm_opportunity_code_seq";
 
     private final DataSource dataSource;
 
@@ -26,37 +28,165 @@ public class OpportunitySchemaInitializer implements ApplicationRunner {
     @Override
     public void run(ApplicationArguments args) {
         try (Connection connection = dataSource.getConnection()) {
-            if (!tableExists(connection, "crm_opportunity")) {
-                return;
+            if (tableExists(connection, "crm_opportunity")) {
+                ensureOpportunitySchema(connection);
             }
-            try (Statement statement = connection.createStatement()) {
-                statement.execute("alter table crm_opportunity add column if not exists pre_sales_id bigint");
-                statement.execute("alter table crm_opportunity add column if not exists pre_sales_name varchar(100)");
-                statement.execute("alter table crm_opportunity add column if not exists project_ownership_location varchar(100)");
-                statement.execute("alter table crm_opportunity add column if not exists latest_progress text");
-                statement.execute("alter table crm_opportunity add column if not exists next_plan text");
-                statement.execute("alter table crm_opportunity add column if not exists updated_by bigint");
-                statement.execute("alter table crm_opportunity add column if not exists archived_at timestamptz");
-                statement.execute("alter table crm_opportunity add column if not exists actual_signed_amount numeric(18, 2)");
-                statement.execute("alter table crm_opportunity add column if not exists is_poc boolean not null default false");
-                statement.execute("create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at)");
-                statement.execute("comment on column crm_opportunity.project_ownership_location is '业绩归属地编码，对应 cnarea.area_code'");
-                statement.execute("comment on column crm_opportunity.latest_progress is '项目最新进展'");
-                statement.execute("comment on column crm_opportunity.next_plan is '下一步销售计划'");
-                statement.execute("comment on column crm_opportunity.updated_by is '更新人ID'");
-                statement.execute("comment on column crm_opportunity.archived_at is '归档时间'");
-                statement.execute("comment on column crm_opportunity.actual_signed_amount is '实际签约金额'");
-                statement.execute("comment on column crm_opportunity.is_poc is '是否POC测试项目'");
-            }
-            ensureArchivedAtStorage(connection);
-            ensureConfidenceGradeStorage(connection);
-            migrateLegacyOmsProjectCode(connection);
-            log.info("Ensured compatibility columns exist for crm_opportunity");
+            ensureSequences(connection);
+            log.info("Ensured CRM compatibility columns and sequences exist");
         } catch (SQLException exception) {
             throw new IllegalStateException("Failed to initialize crm_opportunity schema compatibility", exception);
         }
     }
 
+    private void ensureOpportunitySchema(Connection connection) throws SQLException {
+        try (Statement statement = connection.createStatement()) {
+            statement.execute("alter table crm_opportunity add column if not exists pre_sales_id bigint");
+            statement.execute("alter table crm_opportunity add column if not exists pre_sales_name varchar(100)");
+            statement.execute("alter table crm_opportunity add column if not exists project_ownership_location varchar(100)");
+            statement.execute("alter table crm_opportunity add column if not exists latest_progress text");
+            statement.execute("alter table crm_opportunity add column if not exists next_plan text");
+            statement.execute("alter table crm_opportunity add column if not exists updated_by bigint");
+            statement.execute("alter table crm_opportunity add column if not exists archived_at timestamptz");
+            statement.execute("alter table crm_opportunity add column if not exists actual_signed_amount numeric(18, 2)");
+            statement.execute("alter table crm_opportunity add column if not exists is_poc boolean not null default false");
+            statement.execute("create index if not exists idx_crm_opportunity_archived_at on crm_opportunity(archived_at)");
+            statement.execute("comment on column crm_opportunity.project_ownership_location is '业绩归属地编码，对应 cnarea.area_code'");
+            statement.execute("comment on column crm_opportunity.latest_progress is '项目最新进展'");
+            statement.execute("comment on column crm_opportunity.next_plan is '下一步销售计划'");
+            statement.execute("comment on column crm_opportunity.updated_by is '更新人ID'");
+            statement.execute("comment on column crm_opportunity.archived_at is '归档时间'");
+            statement.execute("comment on column crm_opportunity.actual_signed_amount is '实际签约金额'");
+            statement.execute("comment on column crm_opportunity.is_poc is '是否POC测试项目'");
+        }
+        ensureArchivedAtStorage(connection);
+        ensureConfidenceGradeStorage(connection);
+        migrateLegacyOmsProjectCode(connection);
+    }
+
+    private void ensureSequences(Connection connection) throws SQLException {
+        boolean originalAutoCommit = connection.getAutoCommit();
+        try {
+            connection.setAutoCommit(false);
+            if (tableExists(connection, "crm_customer")) {
+                ensureCodeSequence(connection, CUSTOMER_CODE_SEQUENCE, "crm_customer", "customer_code", "CUS");
+            }
+            if (tableExists(connection, "crm_opportunity")) {
+                ensureCodeSequence(connection, OPPORTUNITY_CODE_SEQUENCE, "crm_opportunity", "opportunity_code", "OPP");
+            }
+            if (tableExists(connection, "crm_sales_expansion")) {
+                ensureIdentitySequence(connection, "crm_sales_expansion", "id");
+            }
+            if (tableExists(connection, "crm_channel_expansion")) {
+                ensureIdentitySequence(connection, "crm_channel_expansion", "id");
+            }
+            connection.commit();
+        } catch (SQLException exception) {
+            rollback(connection, exception);
+            throw exception;
+        } finally {
+            connection.setAutoCommit(originalAutoCommit);
+        }
+    }
+
+    private void ensureCodeSequence(
+            Connection connection,
+            String sequenceName,
+            String tableName,
+            String columnName,
+            String codePrefix) throws SQLException {
+        try (Statement statement = connection.createStatement()) {
+            statement.execute("create sequence if not exists " + sequenceName + " start with 1 increment by 1 minvalue 1");
+        }
+
+        lockSequence(connection, sequenceName);
+        long maxSuffix = selectMaxNumericSuffix(connection, tableName, columnName, codePrefix);
+        long nextSequenceValue = selectNextSequenceValue(connection, sequenceName);
+        if (nextSequenceValue <= maxSuffix) {
+            restartSequence(connection, sequenceName, increment(maxSuffix, sequenceName));
+        }
+    }
+
+    private void ensureIdentitySequence(Connection connection, String tableName, String columnName) throws SQLException {
+        String sequenceName = selectIdentitySequenceName(connection, tableName, columnName);
+        if (sequenceName == null || sequenceName.isBlank()) {
+            return;
+        }
+        lockSequence(connection, sequenceName);
+        long maxId = selectMaxId(connection, tableName, columnName);
+        long nextSequenceValue = selectNextSequenceValue(connection, sequenceName);
+        if (nextSequenceValue <= maxId) {
+            restartSequence(connection, sequenceName, increment(maxId, sequenceName));
+        }
+    }
+
+    private String selectIdentitySequenceName(Connection connection, String tableName, String columnName) throws SQLException {
+        try (PreparedStatement statement = connection.prepareStatement("select pg_get_serial_sequence(?, ?)")) {
+            statement.setString(1, tableName);
+            statement.setString(2, columnName);
+            try (ResultSet resultSet = statement.executeQuery()) {
+                return resultSet.next() ? resultSet.getString(1) : null;
+            }
+        }
+    }
+
+    private long selectMaxId(Connection connection, String tableName, String columnName) throws SQLException {
+        try (Statement statement = connection.createStatement();
+             ResultSet resultSet = statement.executeQuery(
+                     "select coalesce(max(" + columnName + "), 0) from " + tableName)) {
+            return resultSet.next() ? resultSet.getLong(1) : 0;
+        }
+    }
+
+    private void lockSequence(Connection connection, String sequenceName) throws SQLException {
+        try (Statement statement = connection.createStatement()) {
+            statement.execute("alter sequence " + sequenceName + " no cycle");
+        }
+    }
+
+    private void restartSequence(Connection connection, String sequenceName, long nextValue) throws SQLException {
+        try (Statement statement = connection.createStatement()) {
+            statement.execute("alter sequence " + sequenceName + " restart with " + nextValue);
+        }
+    }
+
+    private long increment(long value, String sequenceName) throws SQLException {
+        if (value == Long.MAX_VALUE) {
+            throw new SQLException("Sequence " + sequenceName + " has exhausted bigint values");
+        }
+        return value + 1;
+    }
+
+    private void rollback(Connection connection, SQLException originalException) {
+        try {
+            connection.rollback();
+        } catch (SQLException rollbackException) {
+            originalException.addSuppressed(rollbackException);
+        }
+    }
+
+    private long selectMaxNumericSuffix(
+            Connection connection,
+            String tableName,
+            String columnName,
+            String codePrefix) throws SQLException {
+        String sql = "select coalesce(max((substring(" + columnName + " from '([0-9]+)$'))::bigint), 0) from " + tableName
+                + " where " + columnName + " ~ '^" + codePrefix + "-[0-9]{8}-[0-9]+$'";
+        try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(sql)) {
+            return resultSet.next() ? resultSet.getLong(1) : 0;
+        }
+    }
+
+    private long selectNextSequenceValue(Connection connection, String sequenceName) throws SQLException {
+        try (Statement statement = connection.createStatement();
+             ResultSet resultSet = statement.executeQuery(
+                     "select case when is_called then last_value + 1 else last_value end from " + sequenceName)) {
+            if (!resultSet.next()) {
+                throw new SQLException("Unable to read sequence " + sequenceName);
+            }
+            return resultSet.getLong(1);
+        }
+    }
+
     private void ensureArchivedAtStorage(Connection connection) throws SQLException {
         try (Statement statement = connection.createStatement()) {
             statement.execute("""
diff --git a/backend/src/main/java/com/unis/crm/controller/ExpansionController.java b/backend/src/main/java/com/unis/crm/controller/ExpansionController.java
index 4212f233..6f437889 100644
--- a/backend/src/main/java/com/unis/crm/controller/ExpansionController.java
+++ b/backend/src/main/java/com/unis/crm/controller/ExpansionController.java
@@ -1,6 +1,7 @@
 package com.unis.crm.controller;
 
 import com.unis.crm.common.ApiResponse;
+import com.unis.crm.common.CrmIdJacksonConfig;
 import com.unis.crm.common.CurrentUserUtils;
 import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
 import com.unis.crm.dto.expansion.CreateExpansionFollowUpRequest;
@@ -85,18 +86,20 @@ public class ExpansionController {
 
     @PostMapping("/sales")
     @Log(type = "拓展管理", value = "新增售前拓展")
-    public ApiResponse<Long> createSales(
+    public ApiResponse<Object> createSales(
             @RequestHeader("X-User-Id") Long userId,
             @Valid @RequestBody CreateSalesExpansionRequest request) {
-        return ApiResponse.success(expansionService.createSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), request));
+        Long id = expansionService.createSalesExpansion(CurrentUserUtils.requireCurrentUserId(userId), request);
+        return ApiResponse.success(CrmIdJacksonConfig.toJsonCompatibleValue(id));
     }
 
     @PostMapping("/channel")
     @Log(type = "拓展管理", value = "新增渠道拓展")
-    public ApiResponse<Long> createChannel(
+    public ApiResponse<Object> createChannel(
             @RequestHeader("X-User-Id") Long userId,
             @Valid @RequestBody CreateChannelExpansionRequest request) {
-        return ApiResponse.success(expansionService.createChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), request));
+        Long id = expansionService.createChannelExpansion(CurrentUserUtils.requireCurrentUserId(userId), request);
+        return ApiResponse.success(CrmIdJacksonConfig.toJsonCompatibleValue(id));
     }
 
     @PutMapping("/sales/{id}")
diff --git a/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java b/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java
index 3e72cd9f..f6eee155 100644
--- a/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java
+++ b/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java
@@ -91,6 +91,10 @@ public interface OpportunityMapper {
     @DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
     int countOwnedOpportunity(@Param("userId") Long userId, @Param("id") Long id);
 
+    int countSalesExpansionById(@Param("id") Long id);
+
+    int countChannelExpansionById(@Param("id") Long id);
+
     @DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
     Boolean selectArchived(@Param("userId") Long userId, @Param("id") Long id);
 
diff --git a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java
index f7e8766a..10e222a3 100644
--- a/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java
+++ b/backend/src/main/java/com/unis/crm/service/impl/OpportunityServiceImpl.java
@@ -560,6 +560,16 @@ public class OpportunityServiceImpl implements OpportunityService {
         request.setLatestProgress(normalizeSnapshotText(request.getLatestProgress()));
         request.setNextPlan(normalizeSnapshotText(request.getNextPlan()));
         validateOperatorRelations(request.getOperatorName(), request.getSalesExpansionId(), request.getChannelExpansionId());
+        validateExpansionRelations(request.getSalesExpansionId(), request.getChannelExpansionId());
+    }
+
+    private void validateExpansionRelations(Long salesExpansionId, Long channelExpansionId) {
+        if (salesExpansionId != null && opportunityMapper.countSalesExpansionById(salesExpansionId) <= 0) {
+            throw new BusinessException("所选销售拓展不存在或已失效，请重新选择");
+        }
+        if (channelExpansionId != null && opportunityMapper.countChannelExpansionById(channelExpansionId) <= 0) {
+            throw new BusinessException("所选渠道不存在或已失效，请重新选择");
+        }
     }
 
     private String normalizeSnapshotText(String value) {
diff --git a/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml b/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml
index 0abb7002..128f8529 100644
--- a/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml
+++ b/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml
@@ -486,7 +486,7 @@
             updated_at
         ) values (
             #{id},
-            'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_customer), 0) + 1)::text, 3, '0'),
+            'CUS-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad(nextval('crm_customer_code_seq')::text, 6, '0'),
             #{customerName},
             #{userId},
             coalesce(#{source}, '主动开发'),
@@ -525,7 +525,7 @@
             created_at,
             updated_at
         ) values (
-            'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad((coalesce((select count(1) from crm_opportunity), 0) + 1)::text, 3, '0'),
+            'OPP-' || to_char(current_date, 'YYYYMMDD') || '-' || lpad(nextval('crm_opportunity_code_seq')::text, 6, '0'),
             #{request.opportunityName},
             #{customerId},
             #{userId},
@@ -564,6 +564,18 @@
         where o.id = #{id}
     </select>
 
+    <select id="countSalesExpansionById" resultType="int">
+        select count(1)
+        from crm_sales_expansion
+        where id = #{id}
+    </select>
+
+    <select id="countChannelExpansionById" resultType="int">
+        select count(1)
+        from crm_channel_expansion
+        where id = #{id}
+    </select>
+
     <select id="selectArchived" resultType="java.lang.Boolean">
         select coalesce(archived, false)
         from crm_opportunity o
diff --git a/backend/src/test/java/com/unis/crm/common/CrmIdJacksonConfigTest.java b/backend/src/test/java/com/unis/crm/common/CrmIdJacksonConfigTest.java
new file mode 100644
index 00000000..428f2bf8
--- /dev/null
+++ b/backend/src/test/java/com/unis/crm/common/CrmIdJacksonConfigTest.java
@@ -0,0 +1,67 @@
+package com.unis.crm.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.unis.crm.dto.opportunity.CreateOpportunityRequest;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class CrmIdJacksonConfigTest {
+
+    @Test
+    void shouldSerializeLongIdsAsStringsWithoutChangingCounts() throws Exception {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.registerModule(new CrmIdJacksonConfig().crmIdSerializationModule());
+
+        JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(new TestPayload()));
+
+        assertEquals("2074420816598036481", json.path("id").asText());
+        assertTrue(json.path("id").isTextual());
+        assertEquals(50L, json.path("count").asLong());
+        assertTrue(json.path("count").isIntegralNumber());
+        assertEquals(42L, json.path("userId").asLong());
+        assertTrue(json.path("userId").isIntegralNumber());
+        assertEquals("2074420816598036481", json.path("ownerUserIds").get(0).asText());
+        assertTrue(json.path("ownerUserIds").get(0).isTextual());
+    }
+
+    @Test
+    void shouldDeserializeStringIdIntoLongRequestFieldWithoutPrecisionLoss() throws Exception {
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        CreateOpportunityRequest request = objectMapper.readValue(
+                "{\"channelExpansionId\":\"2074420816598036481\"}",
+                CreateOpportunityRequest.class);
+
+        assertEquals(2_074_420_816_598_036_481L, request.getChannelExpansionId());
+    }
+
+    @Test
+    void shouldKeepSafeResponseIdsNumericAndConvertUnsafeIdsToStrings() {
+        assertEquals(42L, CrmIdJacksonConfig.toJsonCompatibleValue(42L));
+        assertEquals(
+                "2074420816598036481",
+                CrmIdJacksonConfig.toJsonCompatibleValue(2_074_420_816_598_036_481L));
+    }
+
+    static class TestPayload {
+        public Long getId() {
+            return 2_074_420_816_598_036_481L;
+        }
+
+        public Long getCount() {
+            return 50L;
+        }
+
+        public Long getUserId() {
+            return 42L;
+        }
+
+        public List<Long> getOwnerUserIds() {
+            return List.of(2_074_420_816_598_036_481L);
+        }
+    }
+}
diff --git a/backend/src/test/java/com/unis/crm/controller/ExpansionControllerIdContractTest.java b/backend/src/test/java/com/unis/crm/controller/ExpansionControllerIdContractTest.java
new file mode 100644
index 00000000..f8fa7aca
--- /dev/null
+++ b/backend/src/test/java/com/unis/crm/controller/ExpansionControllerIdContractTest.java
@@ -0,0 +1,48 @@
+package com.unis.crm.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.Mockito.when;
+
+import com.unis.crm.common.ApiResponse;
+import com.unis.crm.dto.expansion.CreateChannelExpansionRequest;
+import com.unis.crm.dto.expansion.CreateSalesExpansionRequest;
+import com.unis.crm.service.ExpansionService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class ExpansionControllerIdContractTest {
+
+    @Mock
+    private ExpansionService expansionService;
+
+    @InjectMocks
+    private ExpansionController expansionController;
+
+    @Test
+    void createSalesShouldKeepSafeIdNumeric() {
+        CreateSalesExpansionRequest request = new CreateSalesExpansionRequest();
+        when(expansionService.createSalesExpansion(1L, request)).thenReturn(42L);
+
+        ApiResponse<Object> response = expansionController.createSales(1L, request);
+
+        assertInstanceOf(Long.class, response.getData());
+        assertEquals(42L, response.getData());
+    }
+
+    @Test
+    void createChannelShouldReturnUnsafeIdAsString() {
+        CreateChannelExpansionRequest request = new CreateChannelExpansionRequest();
+        long unsafeId = 2_074_420_816_598_036_481L;
+        when(expansionService.createChannelExpansion(1L, request)).thenReturn(unsafeId);
+
+        ApiResponse<Object> response = expansionController.createChannel(1L, request);
+
+        assertInstanceOf(String.class, response.getData());
+        assertEquals("2074420816598036481", response.getData());
+    }
+}
diff --git a/backend/src/test/java/com/unis/crm/controller/ProtocolExceptionHandlingWebMvcTest.java b/backend/src/test/java/com/unis/crm/controller/ProtocolExceptionHandlingWebMvcTest.java
index 92d42c3d..a359e791 100644
--- a/backend/src/test/java/com/unis/crm/controller/ProtocolExceptionHandlingWebMvcTest.java
+++ b/backend/src/test/java/com/unis/crm/controller/ProtocolExceptionHandlingWebMvcTest.java
@@ -3,13 +3,16 @@ package com.unis.crm.controller;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
 import com.unis.crm.common.CrmGlobalExceptionHandler;
+import java.sql.SQLException;
 import java.util.Map;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.MediaType;
+import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -44,6 +47,22 @@ class ProtocolExceptionHandlingWebMvcTest {
                 .andExpect(status().isUnsupportedMediaType());
     }
 
+    @Test
+    void databaseConstraintDetailsShouldNotLeakToClient() throws Exception {
+        mockMvc.perform(get("/protocol-test/database-error"))
+                .andExpect(status().isConflict())
+                .andExpect(jsonPath("$.msg").value("所选渠道不存在或已失效，请重新选择"))
+                .andExpect(jsonPath("$.message").value("所选渠道不存在或已失效，请重新选择"));
+    }
+
+    @Test
+    void unexpectedExceptionDetailsShouldNotLeakToClient() throws Exception {
+        mockMvc.perform(get("/protocol-test/unexpected-error"))
+                .andExpect(status().isInternalServerError())
+                .andExpect(jsonPath("$.msg").value("系统内部错误，请稍后重试"))
+                .andExpect(jsonPath("$.message").value("系统内部错误，请稍后重试"));
+    }
+
     @RestController
     static class ProtocolTestController {
 
@@ -51,5 +70,17 @@ class ProtocolExceptionHandlingWebMvcTest {
         public Map<String, Object> jsonOnly(@RequestBody Map<String, Object> body) {
             return body;
         }
+
+        @GetMapping("/protocol-test/database-error")
+        public void databaseError() {
+            throw new DataIntegrityViolationException(
+                    "mapper SQL leaked",
+                    new SQLException("violates constraint fk_crm_opportunity_channel_expansion"));
+        }
+
+        @GetMapping("/protocol-test/unexpected-error")
+        public void unexpectedError() {
+            throw new IllegalStateException("sensitive mapper and SQL details");
+        }
     }
 }
diff --git a/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java b/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java
index e1112848..e8d38297 100644
--- a/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java
+++ b/backend/src/test/java/com/unis/crm/service/impl/OpportunityServiceImplTest.java
@@ -121,6 +121,24 @@ class OpportunityServiceImplTest {
         verify(opportunityMapper, never()).insertOpportunity(any(), any(), any());
     }
 
+    @Test
+    void createOpportunity_shouldRejectMissingChannelBeforeWriting() {
+        when(permissionService.hasPermi("opportunity:create")).thenReturn(true);
+        long channelId = 2_074_420_816_598_036_481L;
+        when(opportunityMapper.countChannelExpansionById(channelId)).thenReturn(0);
+        CreateOpportunityRequest request = updateRequest("客户A");
+        request.setOperatorName("渠道");
+        request.setChannelExpansionId(channelId);
+
+        BusinessException exception = assertThrows(
+                BusinessException.class,
+                () -> opportunityService.createOpportunity(1L, request));
+
+        assertEquals("所选渠道不存在或已失效，请重新选择", exception.getMessage());
+        verify(opportunityMapper, never()).insertCustomer(any(), any(), any(), any());
+        verify(opportunityMapper, never()).insertOpportunity(any(), any(), any());
+    }
+
     @Test
     void updateOpportunity_shouldKeepCurrentCustomerWhenCustomerNameUnchanged() {
         when(opportunityMapper.countOwnedOpportunity(1L, 10L)).thenReturn(1);
diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts
index 3e20b088..57daff21 100644
--- a/frontend/src/lib/auth.ts
+++ b/frontend/src/lib/auth.ts
@@ -1,10 +1,12 @@
+export type CrmId = string | number;
+
 export interface CaptchaResponse {
   captchaId: string;
   imageBase64: string;
 }
 
 export interface TenantInfo {
-  tenantId: number;
+  tenantId: CrmId;
   tenantCode: string;
   tenantName: string;
 }
@@ -26,8 +28,8 @@ export interface LoginPayload {
 }
 
 export interface UserProfile {
-  userId: number;
-  tenantId: number;
+  userId: CrmId;
+  tenantId: CrmId;
   username: string;
   displayName: string;
   email?: string;
@@ -41,14 +43,14 @@ export interface UserProfile {
 }
 
 export interface UserRole {
-  roleId?: number;
+  roleId?: CrmId;
   roleCode?: string;
   roleName?: string;
 }
 
 export interface UserPermission {
-  permId?: number;
-  parentId?: number;
+  permId?: CrmId;
+  parentId?: CrmId;
   name?: string;
   code: string;
   permType?: string;
@@ -57,12 +59,12 @@ export interface UserPermission {
 }
 
 export interface AdminUserSummary {
-  userId: number;
+  userId: CrmId;
   username: string;
   displayName: string;
   status?: number;
-  tenantId?: number;
-  orgId?: number;
+  tenantId?: CrmId;
+  orgId?: CrmId;
   isAdmin?: boolean;
   isPlatformAdmin?: boolean;
   roleCodes?: string[];
@@ -108,13 +110,13 @@ export interface DashboardTodo {
 }
 
 export interface DashboardActivity {
-  id: number;
+  id: CrmId;
   bizType?: string;
-  bizId?: number;
+  bizId?: CrmId;
   actionType?: string;
   title?: string;
   content?: string;
-  operatorUserId?: number;
+  operatorUserId?: CrmId;
   operatorName?: string;
   targetTab?: string;
   createdAt?: string;
@@ -122,13 +124,13 @@ export interface DashboardActivity {
 }
 
 export interface DashboardMessage {
-  id: number;
-  reportId?: number;
-  senderUserId?: number;
+  id: CrmId;
+  reportId?: CrmId;
+  senderUserId?: CrmId;
   senderName?: string;
   reportDate?: string;
   bizType?: string;
-  bizId?: number;
+  bizId?: CrmId;
   bizName?: string;
   content?: string;
   lineIndexes?: string;
@@ -138,7 +140,7 @@ export interface DashboardMessage {
 }
 
 export interface DashboardAnalyticsCard {
-  id?: number;
+  id?: CrmId;
   cardKey?: string;
   groupName?: string;
   title?: string;
@@ -180,7 +182,7 @@ export interface DashboardAnalyticsPanel {
 }
 
 export interface DashboardHome {
-  userId?: number;
+  userId?: CrmId;
   realName?: string;
   jobTitle?: string;
   deptName?: string;
@@ -197,7 +199,7 @@ export interface DashboardHome {
 }
 
 export interface ProfileOverview {
-  userId?: number;
+  userId?: CrmId;
   monthlyOpportunityCount?: number;
   monthlyExpansionCount?: number;
   averageScore?: number;
@@ -209,14 +211,14 @@ export interface ProfileOverview {
 }
 
 export interface UpdateCurrentUserProfilePayload {
-  userId?: number;
+  userId?: CrmId;
   username?: string;
   displayName?: string;
   email?: string;
   phone?: string;
   pwdResetRequired?: number;
   isPlatformAdmin?: boolean;
-  orgId?: number;
+  orgId?: CrmId;
 }
 
 export interface UpdateCurrentUserPasswordPayload {
@@ -230,17 +232,17 @@ export interface OwnerTransferConflict {
 }
 
 export interface OwnerTransferItem {
-  id: number;
+  id: CrmId;
   name: string;
   code?: string;
   conflict: boolean;
 }
 
 export interface OwnerTransferPreview {
-  tenantId: number;
-  fromUserId: number;
+  tenantId: CrmId;
+  fromUserId: CrmId;
   fromUserName: string;
-  toUserId: number;
+  toUserId: CrmId;
   toUserName: string;
   opportunityCount: number;
   salesExpansionCount: number;
@@ -252,14 +254,14 @@ export interface OwnerTransferPreview {
 }
 
 export interface OwnerTransferSelection {
-  opportunityIds?: number[];
-  salesExpansionIds?: number[];
-  channelExpansionIds?: number[];
+  opportunityIds?: CrmId[];
+  salesExpansionIds?: CrmId[];
+  channelExpansionIds?: CrmId[];
 }
 
 export interface OwnerTransferPayload {
-  fromUserId: number;
-  toUserId: number;
+  fromUserId: CrmId;
+  toUserId: CrmId;
   transferOpportunities: boolean;
   transferSalesExpansions: boolean;
   transferChannelExpansions: boolean;
@@ -267,16 +269,16 @@ export interface OwnerTransferPayload {
 }
 
 export interface OwnerTransferResult {
-  tenantId: number;
-  fromUserId: number;
-  toUserId: number;
+  tenantId: CrmId;
+  fromUserId: CrmId;
+  toUserId: CrmId;
   transferredOpportunityCount: number;
   transferredSalesExpansionCount: number;
   transferredChannelExpansionCount: number;
 }
 
 export interface WorkCheckIn {
-  id: number;
+  id: CrmId;
   date?: string;
   time?: string;
   locationText?: string;
@@ -286,14 +288,14 @@ export interface WorkCheckIn {
   latitude?: number;
   photoUrls?: string[];
   bizType?: "sales" | "channel" | "opportunity";
-  bizId?: number;
+  bizId?: CrmId;
   bizName?: string;
   userName?: string;
   deptName?: string;
 }
 
 export interface WorkDailyReport {
-  id: number;
+  id: CrmId;
   date?: string;
   submitTime?: string;
   workContent?: string;
@@ -308,7 +310,7 @@ export interface WorkDailyReport {
 }
 
 export interface WorkHistoryItem {
-  id: number;
+  id: CrmId;
   type?: string;
   date?: string;
   time?: string;
@@ -333,7 +335,7 @@ export interface UploadReportAttachmentOptions {
 }
 
 export interface WorkReportToUser {
-  userId: number;
+  userId: CrmId;
   name: string;
   username?: string;
 }
@@ -411,7 +413,7 @@ export interface CreateWorkCheckInPayload {
   latitude?: number;
   photoUrls?: string[];
   bizType?: "sales" | "channel" | "opportunity";
-  bizId?: number;
+  bizId?: CrmId;
   bizName?: string;
   userName?: string;
   deptName?: string;
@@ -420,7 +422,7 @@ export interface CreateWorkCheckInPayload {
 export interface WorkReportLineItem {
   workDate: string;
   bizType: "sales" | "channel" | "opportunity";
-  bizId: number;
+  bizId: CrmId;
   bizName?: string;
   editorText?: string;
   content: string;
@@ -449,8 +451,8 @@ export interface CreateWorkDailyReportPayload {
 }
 
 export interface OpportunityFollowUp {
-  id: number;
-  opportunityId?: number;
+  id: CrmId;
+  opportunityId?: CrmId;
   date?: string;
   type?: string;
   content?: string;
@@ -464,8 +466,8 @@ export interface OpportunityFollowUp {
 }
 
 export interface OpportunityItem {
-  id: number;
-  ownerUserId?: number;
+  id: CrmId;
+  ownerUserId?: CrmId;
   code?: string;
   name?: string;
   client?: string;
@@ -490,15 +492,15 @@ export interface OpportunityItem {
   isPoc?: boolean;
   product?: string;
   source?: string;
-  salesExpansionId?: number;
+  salesExpansionId?: CrmId;
   salesExpansionName?: string;
   salesExpansionIntent?: string;
   salesExpansionActive?: boolean;
-  channelExpansionId?: number;
+  channelExpansionId?: CrmId;
   channelExpansionName?: string;
   channelExpansionIntent?: string;
   channelExpansionEstablishedDate?: string;
-  preSalesId?: number;
+  preSalesId?: CrmId;
   preSalesName?: string;
   competitorName?: string;
   latestProgress?: string;
@@ -527,7 +529,7 @@ export interface OpportunityMeta {
 }
 
 export interface OmsPreSalesOption {
-  userId: number;
+  userId: CrmId;
   loginName?: string;
   userName?: string;
 }
@@ -545,8 +547,8 @@ export interface CreateOpportunityPayload {
   opportunityType?: string;
   productType?: string;
   source?: string;
-  salesExpansionId?: number;
-  channelExpansionId?: number;
+  salesExpansionId?: CrmId;
+  channelExpansionId?: CrmId;
   competitorName?: string;
   latestProgress?: string;
   nextPlan?: string;
@@ -555,7 +557,7 @@ export interface CreateOpportunityPayload {
 }
 
 export interface PushOpportunityToOmsPayload {
-  preSalesId?: number;
+  preSalesId?: CrmId;
   preSalesName?: string;
 }
 
@@ -567,8 +569,8 @@ export interface CreateOpportunityFollowUpPayload {
 }
 
 export interface ExpansionFollowUp {
-  id: number;
-  bizId?: number;
+  id: CrmId;
+  bizId?: CrmId;
   bizType?: string;
   date?: string;
   type?: string;
@@ -580,8 +582,8 @@ export interface ExpansionFollowUp {
 }
 
 export interface SalesExpansionItem {
-  id: number;
-  ownerUserId?: number;
+  id: CrmId;
+  ownerUserId?: CrmId;
   owner?: string;
   type: "sales";
   createdAt?: string;
@@ -612,7 +614,7 @@ export interface SalesExpansionItem {
 }
 
 export interface RelatedProjectSummary {
-  opportunityId: number;
+  opportunityId: CrmId;
   opportunityCode?: string;
   opportunityName?: string;
   stageCode?: string;
@@ -621,8 +623,8 @@ export interface RelatedProjectSummary {
 }
 
 export interface ChannelExpansionItem {
-  id: number;
-  ownerUserId?: number;
+  id: CrmId;
+  ownerUserId?: CrmId;
   owner?: string;
   type: "channel";
   createdAt?: string;
@@ -662,14 +664,14 @@ export interface ChannelExpansionItem {
 }
 
 export interface ChannelExpansionContact {
-  id?: number;
+  id?: CrmId;
   name?: string;
   mobile?: string;
   title?: string;
 }
 
 export interface ChannelRelatedProjectSummary {
-  opportunityId: number;
+  opportunityId: CrmId;
   opportunityCode?: string;
   opportunityName?: string;
   stageCode?: string;
@@ -843,8 +845,8 @@ const AUTH_REQUIRED_MESSAGE_PATTERNS = [
 
 type AccessTokenPayload = {
   exp?: number;
-  userId?: number;
-  tenantId?: number;
+  userId?: CrmId;
+  tenantId?: CrmId;
 };
 
 function isSuccessCode(code: unknown) {
@@ -1223,8 +1225,9 @@ function getStoredUserId() {
     const rawProfile = sessionStorage.getItem("userProfile");
     if (rawProfile) {
       const profile = JSON.parse(rawProfile) as Partial<UserProfile>;
-      if (typeof profile.userId === "number" && Number.isFinite(profile.userId)) {
-        return profile.userId;
+      const profileUserId = normalizeStoredId(profile.userId);
+      if (profileUserId) {
+        return profileUserId;
       }
     }
   } catch {
@@ -1238,8 +1241,9 @@ function getStoredUserId() {
     }
 
     const tokenPayload = getAccessTokenPayload(token);
-    if (typeof tokenPayload.userId === "number" && Number.isFinite(tokenPayload.userId)) {
-      return tokenPayload.userId;
+    const tokenUserId = normalizeStoredId(tokenPayload.userId);
+    if (tokenUserId) {
+      return tokenUserId;
     }
   } catch {
     return undefined;
@@ -1248,6 +1252,16 @@ function getStoredUserId() {
   return undefined;
 }
 
+function normalizeStoredId(value: unknown): CrmId | undefined {
+  if (typeof value === "string" && value.trim()) {
+    return value.trim();
+  }
+  if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
+    return value;
+  }
+  return undefined;
+}
+
 export function getStoredCurrentUserId() {
   return getStoredUserId();
 }
@@ -1369,7 +1383,7 @@ export async function completeDashboardTodo(todoId: string) {
   }, true);
 }
 
-export async function readDashboardMessage(messageId: number) {
+export async function readDashboardMessage(messageId: CrmId) {
   return request<void>(`/api/dashboard/messages/${messageId}/read`, {
     method: "POST",
   }, true);
@@ -1398,14 +1412,14 @@ export async function updateCurrentUserPassword(payload: UpdateCurrentUserPasswo
   }, true);
 }
 
-export async function updateUserProfileById(userId: number, payload: UpdateCurrentUserProfilePayload) {
+export async function updateUserProfileById(userId: CrmId, payload: UpdateCurrentUserProfilePayload) {
   return request<boolean>(`/api/sys/api/users/${userId}`, {
     method: "PUT",
     body: JSON.stringify(payload),
   }, true);
 }
 
-export async function listAdminUsers(params?: { tenantId?: number; orgId?: number }) {
+export async function listAdminUsers(params?: { tenantId?: CrmId; orgId?: CrmId }) {
   const searchParams = new URLSearchParams();
   if (params?.tenantId !== undefined) {
     searchParams.set("tenantId", String(params.tenantId));
@@ -1417,7 +1431,7 @@ export async function listAdminUsers(params?: { tenantId?: number; orgId?: numbe
   return request<AdminUserSummary[]>(`/api/sys/api/users${query ? `?${query}` : ""}`, undefined, true);
 }
 
-export async function previewOwnerTransfer(fromUserId: number, toUserId: number) {
+export async function previewOwnerTransfer(fromUserId: CrmId, toUserId: CrmId) {
   const params = new URLSearchParams({
     fromUserId: String(fromUserId),
     toUserId: String(toUserId),
@@ -1485,7 +1499,7 @@ export async function getWorkHistory(type: "checkin" | "report", page = 1, size
   return request<WorkHistoryPage>(`/api/work/history?${params.toString()}`, undefined, true);
 }
 
-export async function getWorkReportHistoryItem(reportId: number) {
+export async function getWorkReportHistoryItem(reportId: CrmId) {
   return request<WorkHistoryItem>(`/api/work/history/reports/${reportId}`, undefined, true);
 }
 
@@ -1605,7 +1619,7 @@ export async function getOpportunityOverview(keyword?: string, stage?: string) {
   return request<OpportunityOverview>(`/api/opportunities/overview${query ? `?${query}` : ""}`, undefined, true);
 }
 
-export async function getOpportunityDetail(opportunityId: number) {
+export async function getOpportunityDetail(opportunityId: CrmId) {
   return request<OpportunityItem>(`/api/opportunities/${opportunityId}`, undefined, true);
 }
 
@@ -1624,21 +1638,21 @@ export async function createOpportunity(payload: CreateOpportunityPayload) {
   }, true);
 }
 
-export async function updateOpportunity(opportunityId: number, payload: CreateOpportunityPayload) {
+export async function updateOpportunity(opportunityId: CrmId, payload: CreateOpportunityPayload) {
   return request<number>(`/api/opportunities/${opportunityId}`, {
     method: "PUT",
     body: JSON.stringify(payload),
   }, true);
 }
 
-export async function pushOpportunityToOms(opportunityId: number, payload?: PushOpportunityToOmsPayload) {
+export async function pushOpportunityToOms(opportunityId: CrmId, payload?: PushOpportunityToOmsPayload) {
   return request<number>(`/api/opportunities/${opportunityId}/push-oms`, {
     method: "POST",
     body: JSON.stringify(payload ?? {}),
   }, true);
 }
 
-export async function createOpportunityFollowUp(opportunityId: number, payload: CreateOpportunityFollowUpPayload) {
+export async function createOpportunityFollowUp(opportunityId: CrmId, payload: CreateOpportunityFollowUpPayload) {
   return request<number>(`/api/opportunities/${opportunityId}/followups`, {
     method: "POST",
     body: JSON.stringify(payload),
@@ -1675,7 +1689,7 @@ export async function getExpansionCityOptions(provinceName: string) {
   return request<ExpansionDictOption[]>(`/api/expansion/areas/cities?${params.toString()}`, undefined, true);
 }
 
-export async function checkSalesExpansionDuplicate(employeeNo: string, excludeId?: number) {
+export async function checkSalesExpansionDuplicate(employeeNo: string, excludeId?: CrmId) {
   const params = new URLSearchParams({ employeeNo });
   if (excludeId) {
     params.set("excludeId", String(excludeId));
@@ -1683,7 +1697,7 @@ export async function checkSalesExpansionDuplicate(employeeNo: string, excludeId
   return request<ExpansionDuplicateCheck>(`/api/expansion/sales/duplicate-check?${params.toString()}`, undefined, true);
 }
 
-export async function checkChannelExpansionDuplicate(channelName: string, excludeId?: number) {
+export async function checkChannelExpansionDuplicate(channelName: string, excludeId?: CrmId) {
   const params = new URLSearchParams({ channelName });
   if (excludeId) {
     params.set("excludeId", String(excludeId));
@@ -1692,27 +1706,27 @@ export async function checkChannelExpansionDuplicate(channelName: string, exclud
 }
 
 export async function createSalesExpansion(payload: CreateSalesExpansionPayload) {
-  return request<number>("/api/expansion/sales", {
+  return request<CrmId>("/api/expansion/sales", {
     method: "POST",
     body: JSON.stringify(payload),
   }, true);
 }
 
 export async function createChannelExpansion(payload: CreateChannelExpansionPayload) {
-  return request<number>("/api/expansion/channel", {
+  return request<CrmId>("/api/expansion/channel", {
     method: "POST",
     body: JSON.stringify(serializeChannelExpansionPayload(payload)),
   }, true);
 }
 
-export async function updateSalesExpansion(id: number, payload: UpdateSalesExpansionPayload) {
+export async function updateSalesExpansion(id: CrmId, payload: UpdateSalesExpansionPayload) {
   return request<void>(`/api/expansion/sales/${id}`, {
     method: "PUT",
     body: JSON.stringify(payload),
   }, true);
 }
 
-export async function updateChannelExpansion(id: number, payload: UpdateChannelExpansionPayload) {
+export async function updateChannelExpansion(id: CrmId, payload: UpdateChannelExpansionPayload) {
   return request<void>(`/api/expansion/channel/${id}`, {
     method: "PUT",
     body: JSON.stringify(serializeChannelExpansionPayload(payload)),
@@ -1721,7 +1735,7 @@ export async function updateChannelExpansion(id: number, payload: UpdateChannelE
 
 export async function createExpansionFollowUp(
   bizType: "sales" | "channel",
-  bizId: number,
+  bizId: CrmId,
   payload: CreateExpansionFollowUpPayload,
 ) {
   return request<number>(`/api/expansion/${bizType}/${bizId}/followups`, {
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index c720b5ba..bdde74a1 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -715,9 +715,9 @@ export default function Dashboard() {
   const visibleHistoryTodos = showAllHistoryTodos ? historyTodos : historyTodos.slice(0, DASHBOARD_HISTORY_PREVIEW_COUNT);
   const activities = (home.activities?.length
     ? home.activities
-    : [{ id: 0, title: "无", content: "无", timeText: "无" }]) as DashboardActivity[];
+    : [{ id: "0", title: "无", content: "无", timeText: "无" }]) as DashboardActivity[];
   const visibleActivities = showAllActivities ? activities : activities.slice(0, DASHBOARD_PREVIEW_COUNT);
-  const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== 0;
+  const hasMoreActivities = activities.length > DASHBOARD_PREVIEW_COUNT && activities[0]?.id !== "0";
   const hasMoreHistoryTodos = historyTodos.length > DASHBOARD_HISTORY_PREVIEW_COUNT;
   const showStatsCard = home.statsCardVisible !== false;
   const showTodoCard = home.todoCardVisible !== false;
diff --git a/frontend/src/pages/Expansion.tsx b/frontend/src/pages/Expansion.tsx
index 005586d5..e17cc347 100644
--- a/frontend/src/pages/Expansion.tsx
+++ b/frontend/src/pages/Expansion.tsx
@@ -19,6 +19,7 @@ import {
   updateSalesExpansion,
   type ChannelExpansionContact,
   type ChannelExpansionItem,
+  type CrmId,
   type CreateChannelExpansionPayload,
   type CreateSalesExpansionPayload,
   type ExpansionDictOption,
@@ -32,7 +33,7 @@ import { cn } from "@/lib/utils";
 
 type ExpansionItem = SalesExpansionItem | ChannelExpansionItem;
 type ExpansionTab = "sales" | "channel";
-type ExpansionLocationState = { tab?: ExpansionTab; selectedId?: number } | null;
+type ExpansionLocationState = { tab?: ExpansionTab; selectedId?: CrmId } | null;
 type ExpansionExportFilters = {
   keyword?: string;
   intent?: string;
diff --git a/frontend/src/pages/Opportunities.tsx b/frontend/src/pages/Opportunities.tsx
index df7d8712..ec18f964 100644
--- a/frontend/src/pages/Opportunities.tsx
+++ b/frontend/src/pages/Opportunities.tsx
@@ -23,6 +23,7 @@ import {
   updateOpportunity,
   type ChannelExpansionContact,
   type ChannelExpansionItem,
+  type CrmId,
   type CreateChannelExpansionPayload,
   type CreateOpportunityPayload,
   type CreateSalesExpansionPayload,
@@ -86,7 +87,7 @@ const COMPETITOR_OPTIONS = [
 type CompetitorOption = (typeof COMPETITOR_OPTIONS)[number];
 type OperatorMode = "none" | "h3c" | "channel" | "both";
 type OpportunityArchiveTab = "active" | "archived";
-type OpportunityLocationState = { selectedId?: number; archiveTab?: OpportunityArchiveTab } | null;
+type OpportunityLocationState = { selectedId?: CrmId; archiveTab?: OpportunityArchiveTab } | null;
 type OpportunityExportFilters = {
   keyword?: string;
   expectedStartDate?: string;
@@ -769,8 +770,8 @@ function buildOpportunitySubmitPayload(
 
 function validateOperatorRelations(
   operatorMode: OperatorMode,
-  salesExpansionId: number | undefined,
-  channelExpansionId: number | undefined,
+  salesExpansionId: CrmId | undefined,
+  channelExpansionId: CrmId | undefined,
 ) {
   if (operatorMode === "h3c" && !salesExpansionId) {
     throw new Error("运作方选择“新华三”时，新华三负责人必须填写");
@@ -1290,7 +1291,7 @@ function OpportunityExportFilterModal({
 }
 
 type SearchableOption = {
-  value: number | string;
+  value: CrmId;
   label: string;
   keywords?: string[];
 };
@@ -1372,7 +1373,7 @@ function SearchableSelect({
   onCreate,
   onQueryChange,
 }: {
-  value?: number;
+  value?: CrmId;
   options: SearchableOption[];
   placeholder: string;
   searchPlaceholder: string;
@@ -1380,7 +1381,7 @@ function SearchableSelect({
   loading?: boolean;
   createActionLabel?: string;
   className?: string;
-  onChange: (value?: number) => void;
+  onChange: (value?: CrmId) => void;
   onCreate?: (query: string) => void;
   onQueryChange?: (query: string) => void;
 }) {
@@ -1512,7 +1513,7 @@ function SearchableSelect({
               type="button"
               key={item.value}
               onClick={() => {
-                onChange(Number(item.value));
+                onChange(item.value);
                 setOpen(false);
                 resetQuery();
               }}
@@ -2594,14 +2595,14 @@ export default function Opportunities() {
     }
   };
 
-  const handleSalesExpansionChange = (value?: number) => {
+  const handleSalesExpansionChange = (value?: CrmId) => {
     setSelectedSalesExpansionOption(
       value ? salesExpansionSearchOptions.find((option) => option.value === value) ?? null : null,
     );
     handleChange("salesExpansionId", value);
   };
 
-  const handleChannelExpansionChange = (value?: number) => {
+  const handleChannelExpansionChange = (value?: CrmId) => {
     setSelectedChannelExpansionOption(
       value ? channelExpansionSearchOptions.find((option) => option.value === value) ?? null : null,
     );
@@ -2716,7 +2717,7 @@ export default function Opportunities() {
     setQuickCreateSubmitting(true);
 
     try {
-      let createdId: number;
+      let createdId: CrmId;
       if (quickCreateType === "sales") {
         const duplicateResult = await checkSalesExpansionDuplicate(quickSalesForm.employeeNo.trim());
         if (duplicateResult.duplicated) {
@@ -2796,7 +2797,7 @@ export default function Opportunities() {
     setCustomCompetitorName("");
   };
 
-  const reload = async (preferredSelectedId?: number) => {
+  const reload = async (preferredSelectedId?: CrmId) => {
     const data = await getOpportunityOverview(keyword, filter);
     const nextItems = data.items ?? [];
     setItems(nextItems);
diff --git a/frontend/src/pages/OwnerTransfer.tsx b/frontend/src/pages/OwnerTransfer.tsx
index e0c5ae15..9fd93e4f 100644
--- a/frontend/src/pages/OwnerTransfer.tsx
+++ b/frontend/src/pages/OwnerTransfer.tsx
@@ -22,6 +22,7 @@ import {
   listOwnerTransferTargetUsers,
   previewOwnerTransfer,
   type AdminUserSummary,
+  type CrmId,
   type OwnerTransferItem,
   type OwnerTransferPayload,
   type OwnerTransferPreview,
@@ -31,9 +32,9 @@ import { cn } from "@/lib/utils";
 type TransferTab = "opportunities" | "sales" | "channels";
 type TransferKind = "opportunity" | "sales" | "channel";
 type SelectionState = {
-  opportunityIds: number[];
-  salesExpansionIds: number[];
-  channelExpansionIds: number[];
+  opportunityIds: CrmId[];
+  salesExpansionIds: CrmId[];
+  channelExpansionIds: CrmId[];
 };
 
 type TransferConfirmState = {
@@ -61,7 +62,7 @@ const TAB_META: Array<{
 function getActiveTenantId() {
   const rawValue = localStorage.getItem("activeTenantId");
   const tenantId = Number(rawValue || 0);
-  return Number.isFinite(tenantId) ? tenantId : 0;
+  return Number.isSafeInteger(tenantId) ? tenantId : 0;
 }
 
 function getDisplayUserName(user?: AdminUserSummary | null) {
@@ -83,6 +84,18 @@ function countLabel(value?: number) {
   return typeof value === "number" && Number.isFinite(value) ? value : 0;
 }
 
+function normalizeSelectedId(value?: string): CrmId | undefined {
+  const normalized = value?.trim();
+  if (!normalized || normalized === "0") {
+    return undefined;
+  }
+  return normalized;
+}
+
+function isSameCrmId(left?: CrmId | null, right?: CrmId | null) {
+  return left !== undefined && left !== null && right !== undefined && right !== null && String(left) === String(right);
+}
+
 function createEmptySelectionState(): SelectionState {
   return {
     opportunityIds: [],
@@ -145,15 +158,15 @@ export default function OwnerTransfer({
 
   const activeTenantId = getActiveTenantId();
   const tenantSelected = activeTenantId > 0;
-  const numericFromUserId = fromUserId ? Number(fromUserId) : undefined;
-  const numericToUserId = toUserId ? Number(toUserId) : undefined;
+  const selectedFromUserId = normalizeSelectedId(fromUserId);
+  const selectedToUserId = normalizeSelectedId(toUserId);
   const fromUser = useMemo(
-    () => users.find((item) => item.userId === numericFromUserId) ?? currentUser,
-    [currentUser, numericFromUserId, users],
+    () => users.find((item) => isSameCrmId(item.userId, selectedFromUserId)) ?? currentUser,
+    [currentUser, selectedFromUserId, users],
   );
   const toUser = useMemo(
-    () => users.find((item) => item.userId === numericToUserId) ?? null,
-    [numericToUserId, users],
+    () => users.find((item) => isSameCrmId(item.userId, selectedToUserId)) ?? null,
+    [selectedToUserId, users],
   );
   const selectedPreviewHasSalesConflict = Boolean(preview?.salesConflicts?.length);
 
@@ -196,7 +209,7 @@ export default function OwnerTransfer({
     }
   }, [activeTenantId, tenantSelected]);
 
-  const loadPreview = useCallback(async (sourceUserId?: number, targetUserId?: number) => {
+  const loadPreview = useCallback(async (sourceUserId?: CrmId, targetUserId?: CrmId) => {
     if (!currentUser?.userId) {
       setPreview(null);
       return;
@@ -205,10 +218,8 @@ export default function OwnerTransfer({
       !tenantSelected
       || !sourceUserId
       || !targetUserId
-      || sourceUserId <= 0
-      || targetUserId <= 0
-      || sourceUserId === targetUserId
-      || sourceUserId !== currentUser.userId
+      || isSameCrmId(sourceUserId, targetUserId)
+      || !isSameCrmId(sourceUserId, currentUser.userId)
     ) {
       setPreview(null);
       return;
@@ -236,8 +247,8 @@ export default function OwnerTransfer({
   }, [loadUsers]);
 
   useEffect(() => {
-    void loadPreview(numericFromUserId, numericToUserId);
-  }, [loadPreview, numericFromUserId, numericToUserId]);
+    void loadPreview(selectedFromUserId, selectedToUserId);
+  }, [loadPreview, selectedFromUserId, selectedToUserId]);
 
   useEffect(() => {
     setSearchKeyword("");
@@ -351,11 +362,11 @@ export default function OwnerTransfer({
   }, [confirmState, runTransfer]);
 
   const handleExecute = async () => {
-    if (!numericFromUserId || !numericToUserId) {
+    if (!selectedFromUserId || !selectedToUserId) {
       setPageError("请先选择原归属人和新归属人");
       return;
     }
-    if (numericFromUserId === numericToUserId) {
+    if (isSameCrmId(selectedFromUserId, selectedToUserId)) {
       setPageError("原归属人和新归属人不能相同");
       return;
     }
@@ -368,8 +379,8 @@ export default function OwnerTransfer({
       title: "确认执行转移？",
       description: "",
       payload: {
-        fromUserId: numericFromUserId,
-        toUserId: numericToUserId,
+        fromUserId: selectedFromUserId,
+        toUserId: selectedToUserId,
         transferOpportunities: selection.opportunityIds.length > 0,
         transferSalesExpansions: selection.salesExpansionIds.length > 0,
         transferChannelExpansions: selection.channelExpansionIds.length > 0,
@@ -392,7 +403,7 @@ export default function OwnerTransfer({
   };
 
   const handleSingleTransfer = async (kind: TransferKind, item: OwnerTransferItem) => {
-    if (!numericFromUserId || !numericToUserId) {
+    if (!selectedFromUserId || !selectedToUserId) {
       setPageError("请先选择原归属人和新归属人");
       return;
     }
@@ -406,8 +417,8 @@ export default function OwnerTransfer({
       title: "确认转移当前数据？",
       description: "",
       payload: {
-        fromUserId: numericFromUserId,
-        toUserId: numericToUserId,
+        fromUserId: selectedFromUserId,
+        toUserId: selectedToUserId,
         transferOpportunities: kind === "opportunity",
         transferSalesExpansions: kind === "sales",
         transferChannelExpansions: kind === "channel",
@@ -426,7 +437,7 @@ export default function OwnerTransfer({
     });
   };
 
-  const toggleItemSelection = (kind: TransferKind, itemId: number) => {
+  const toggleItemSelection = (kind: TransferKind, itemId: CrmId) => {
     const selectionKey = getSelectionKey(kind);
     setSelection((current) => {
       const currentIds = current[selectionKey];
diff --git a/frontend/src/pages/Work.tsx b/frontend/src/pages/Work.tsx
index 380e4fbb..45148a3d 100644
--- a/frontend/src/pages/Work.tsx
+++ b/frontend/src/pages/Work.tsx
@@ -34,6 +34,7 @@ import {
   type ChannelExpansionItem,
   type ChannelExpansionContact,
   type CreateChannelExpansionPayload,
+  type CrmId,
   type CreateOpportunityPayload,
   type CreateWorkCheckInPayload,
   type CreateWorkDailyReportPayload,
@@ -140,7 +141,7 @@ const LOCATION_DISPLAY_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
 const locationDisplayCache = new Map<string, string>();
 
 type WorkRelationOption = {
-  id: number;
+  id: CrmId;
   label: string;
 };
 
@@ -222,7 +223,7 @@ function createEmptyReportLine(): WorkReportLineItem {
   return {
     workDate: format(new Date(), "yyyy-MM-dd"),
     bizType: "sales",
-    bizId: 0,
+    bizId: "",
     bizName: "",
     editorText: "",
     content: "",
@@ -896,8 +897,10 @@ export default function Work() {
   const routeOpenReportId = useMemo(() => {
     const state = routerLocation.state as { openReportId?: unknown } | null;
     const rawId = state?.openReportId;
-    const numericId = typeof rawId === "number" ? rawId : Number(rawId);
-    return Number.isFinite(numericId) && numericId > 0 ? numericId : null;
+    if (typeof rawId === "string" && rawId.trim()) {
+      return rawId.trim();
+    }
+    return typeof rawId === "number" && Number.isSafeInteger(rawId) && rawId > 0 ? rawId : null;
   }, [routerLocation.state]);
 
   const pickerOptions = useMemo(() => {
@@ -928,9 +931,9 @@ export default function Work() {
     : 0;
   const opportunityItemsById = useMemo(() => {
     const entries = opportunityItems
-      .filter((item): item is OpportunityItem & { id: number } => typeof item.id === "number")
+      .filter((item): item is OpportunityItem & { id: CrmId } => isValidCrmId(item.id))
       .map((item) => [item.id, item] as const);
-    return new Map<number, OpportunityItem>(entries);
+    return new Map<CrmId, OpportunityItem>(entries);
   }, [opportunityItems]);
   const quickOpportunityOperatorMode = useMemo(
     () => resolveOperatorMode(quickOpportunityForm.operatorName, quickOpportunityOperatorOptions),
@@ -2752,7 +2755,7 @@ export default function Work() {
                             ...salesOptions.map((item) => ({ value: String(item.id), label: item.label })),
                           ]}
                           className={getFieldInputClass(Boolean(quickOpportunityFieldErrors.salesExpansionId))}
-                          onChange={(value) => handleQuickOpportunityChange("salesExpansionId", value ? Number(value) : undefined)}
+                          onChange={(value) => handleQuickOpportunityChange("salesExpansionId", value || undefined)}
                         />
                         {quickOpportunityFieldErrors.salesExpansionId ? <p className="text-xs text-rose-500">{quickOpportunityFieldErrors.salesExpansionId}</p> : null}
                       </label>
@@ -2771,7 +2774,7 @@ export default function Work() {
                             ...channelOptions.map((item) => ({ value: String(item.id), label: item.label })),
                           ]}
                           className={getFieldInputClass(Boolean(quickOpportunityFieldErrors.channelExpansionId))}
-                          onChange={(value) => handleQuickOpportunityChange("channelExpansionId", value ? Number(value) : undefined)}
+                          onChange={(value) => handleQuickOpportunityChange("channelExpansionId", value || undefined)}
                         />
                         {quickOpportunityFieldErrors.channelExpansionId ? <p className="text-xs text-rose-500">{quickOpportunityFieldErrors.channelExpansionId}</p> : null}
                       </label>
@@ -5188,9 +5191,9 @@ function hasOnlySeeRole(user: UserProfile | null) {
 }
 
 function buildSalesOptions(items: SalesExpansionItem[]): WorkRelationOption[] {
-  const seenIds = new Set<number>();
+  const seenIds = new Set<CrmId>();
   return items
-    .filter((item): item is SalesExpansionItem & { id: number } => typeof item.id === "number")
+    .filter((item): item is SalesExpansionItem & { id: CrmId } => isValidCrmId(item.id))
     .filter((item) => {
       if (seenIds.has(item.id)) {
         return false;
@@ -5202,9 +5205,9 @@ function buildSalesOptions(items: SalesExpansionItem[]): WorkRelationOption[] {
 }
 
 function buildChannelOptions(items: ChannelExpansionItem[]): WorkRelationOption[] {
-  const seenIds = new Set<number>();
+  const seenIds = new Set<CrmId>();
   return items
-    .filter((item): item is ChannelExpansionItem & { id: number } => typeof item.id === "number")
+    .filter((item): item is ChannelExpansionItem & { id: CrmId } => isValidCrmId(item.id))
     .filter((item) => {
       if (seenIds.has(item.id)) {
         return false;
@@ -5216,9 +5219,9 @@ function buildChannelOptions(items: ChannelExpansionItem[]): WorkRelationOption[
 }
 
 function buildOpportunityOptions(items: OpportunityItem[]): WorkRelationOption[] {
-  const seenIds = new Set<number>();
+  const seenIds = new Set<CrmId>();
   return items
-    .filter((item): item is OpportunityItem & { id: number } => typeof item.id === "number")
+    .filter((item): item is OpportunityItem & { id: CrmId } => isValidCrmId(item.id))
     .filter((item) => {
       if (seenIds.has(item.id)) {
         return false;
@@ -5229,6 +5232,13 @@ function buildOpportunityOptions(items: OpportunityItem[]): WorkRelationOption[]
     .map((item) => ({ id: item.id, label: item.name || `商机#${item.id}` }));
 }
 
+function isValidCrmId(value: CrmId | null | undefined): value is CrmId {
+  if (typeof value === "string") {
+    return Boolean(value.trim());
+  }
+  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
+}
+
 function getBizTypeLabel(bizType: BizType) {
   if (bizType === "sales") {
     return "人员拓展";
diff --git a/frontend1/dist/index.html b/frontend1/dist/index.html
index 6e7cc1a3..3d61e321 100644
--- a/frontend1/dist/index.html
+++ b/frontend1/dist/index.html
@@ -5,7 +5,7 @@
     <link rel="icon" type="image/svg+xml" href="/logo.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>UnisBase - 智能会议系统</title>
-    <script type="module" crossorigin src="/assets/index-BaO1tuFT.js"></script>
+    <script type="module" crossorigin src="/assets/index-CmIS0HGH.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
   </head>
   <body>
diff --git a/frontend1/src/features/owner-transfer/pages/owner-transfer/index.tsx b/frontend1/src/features/owner-transfer/pages/owner-transfer/index.tsx
index 581a7a12..9df70c1b 100644
--- a/frontend1/src/features/owner-transfer/pages/owner-transfer/index.tsx
+++ b/frontend1/src/features/owner-transfer/pages/owner-transfer/index.tsx
@@ -27,7 +27,7 @@ import PageHeader from "@/components/shared/PageHeader";
 import { usePermission } from "@/hooks/usePermission";
 import type { SysUser } from "@/types";
 import { executeOwnerTransfer, previewOwnerTransfer } from "@/features/owner-transfer/api";
-import type { OwnerTransferItem, OwnerTransferPayload, OwnerTransferPreview } from "@/features/owner-transfer/types";
+import type { CrmId, OwnerTransferItem, OwnerTransferPayload, OwnerTransferPreview } from "@/features/owner-transfer/types";
 import "./index.less";
 
 interface FormValues {
@@ -66,6 +66,12 @@ function filterItems(items: OwnerTransferItem[], keyword: string) {
   return items.filter((item) => `${item.name || ""} ${item.code || ""}`.toLowerCase().includes(normalizedKeyword));
 }
 
+function normalizeSelectedIds(ids: React.Key[]): CrmId[] {
+  return ids.filter((id): id is CrmId =>
+    typeof id === "string" || (typeof id === "number" && Number.isSafeInteger(id))
+  );
+}
+
 function hasBlockedTransferRole(user: SysUser) {
   return (user.roles ?? []).some((role) => {
     const normalizedRoleCode = role.roleCode?.trim().toUpperCase();
@@ -282,9 +288,9 @@ export default function OwnerTransferPage() {
       transferChannelExpansions: hasRowSelection ? selectedChannelRowKeys.length > 0 : values.transferChannelExpansions,
       selection: hasRowSelection
         ? {
-            opportunityIds: selectedOpportunityRowKeys.map((id) => Number(id)),
-            salesExpansionIds: selectedSalesRowKeys.map((id) => Number(id)),
-            channelExpansionIds: selectedChannelRowKeys.map((id) => Number(id)),
+            opportunityIds: normalizeSelectedIds(selectedOpportunityRowKeys),
+            salesExpansionIds: normalizeSelectedIds(selectedSalesRowKeys),
+            channelExpansionIds: normalizeSelectedIds(selectedChannelRowKeys),
           }
         : undefined,
     };
@@ -369,9 +375,9 @@ export default function OwnerTransferPage() {
       transferSalesExpansions: selectedSalesRowKeys.length > 0,
       transferChannelExpansions: selectedChannelRowKeys.length > 0,
       selection: {
-        opportunityIds: selectedOpportunityRowKeys.map((id) => Number(id)),
-        salesExpansionIds: selectedSalesRowKeys.map((id) => Number(id)),
-        channelExpansionIds: selectedChannelRowKeys.map((id) => Number(id)),
+        opportunityIds: normalizeSelectedIds(selectedOpportunityRowKeys),
+        salesExpansionIds: normalizeSelectedIds(selectedSalesRowKeys),
+        channelExpansionIds: normalizeSelectedIds(selectedChannelRowKeys),
       },
     };
 
diff --git a/frontend1/src/features/owner-transfer/types.ts b/frontend1/src/features/owner-transfer/types.ts
index 83434da8..7b5f193f 100644
--- a/frontend1/src/features/owner-transfer/types.ts
+++ b/frontend1/src/features/owner-transfer/types.ts
@@ -1,10 +1,12 @@
+export type CrmId = string | number;
+
 export interface OwnerTransferConflict {
   employeeNo: string;
   candidateName: string;
 }
 
 export interface OwnerTransferItem {
-  id: number;
+  id: CrmId;
   name: string;
   code?: string;
   conflict: boolean;
@@ -26,9 +28,9 @@ export interface OwnerTransferPreview {
 }
 
 export interface OwnerTransferSelection {
-  opportunityIds?: number[];
-  salesExpansionIds?: number[];
-  channelExpansionIds?: number[];
+  opportunityIds?: CrmId[];
+  salesExpansionIds?: CrmId[];
+  channelExpansionIds?: CrmId[];
 }
 
 export interface OwnerTransferPayload {
diff --git a/sql/20260713_fix_crm_codes_pg17.sql b/sql/20260713_fix_crm_codes_pg17.sql
new file mode 100644
index 00000000..e8f45b65
--- /dev/null
+++ b/sql/20260713_fix_crm_codes_pg17.sql
@@ -0,0 +1,153 @@
+-- Fix historical customer-code duplicates before restoring the intended constraint.
+-- The first row keeps its business code; later duplicates receive a traceable migration code.
+begin;
+
+set local lock_timeout = '10s';
+set local statement_timeout = '5min';
+
+select pg_advisory_xact_lock(hashtext('20260713_fix_crm_codes_pg17'));
+
+with ranked_customer_codes as (
+  select
+    id,
+    row_number() over (partition by customer_code order by created_at nulls last, id) as row_no
+  from crm_customer
+  where customer_code is not null
+    and btrim(customer_code) <> ''
+)
+update crm_customer customer
+set customer_code = 'CUS-MIG-' || customer.id::text,
+    updated_at = now()
+from ranked_customer_codes duplicate
+where duplicate.id = customer.id
+  and duplicate.row_no > 1;
+
+do $$
+declare
+  constraint_definition text;
+begin
+  select pg_get_constraintdef(oid)
+  into constraint_definition
+  from pg_constraint
+  where conrelid = 'crm_customer'::regclass
+    and conname = 'uk_crm_customer_code';
+
+  if constraint_definition is not null
+     and constraint_definition not ilike 'UNIQUE (customer_code)%' then
+    alter table crm_customer drop constraint uk_crm_customer_code;
+    constraint_definition := null;
+  end if;
+
+  if constraint_definition is null then
+    alter table crm_customer
+      add constraint uk_crm_customer_code unique (customer_code);
+  end if;
+end $$;
+
+create sequence if not exists crm_customer_code_seq start with 1 increment by 1 minvalue 1;
+create sequence if not exists crm_opportunity_code_seq start with 1 increment by 1 minvalue 1;
+
+-- Lock sequence consumers before reading their current values.
+alter sequence crm_customer_code_seq no cycle;
+alter sequence crm_opportunity_code_seq no cycle;
+
+do $$
+declare
+  sales_id_sequence regclass;
+  channel_id_sequence regclass;
+begin
+  if to_regclass('crm_sales_expansion') is not null then
+    sales_id_sequence := pg_get_serial_sequence('crm_sales_expansion', 'id')::regclass;
+  end if;
+  if to_regclass('crm_channel_expansion') is not null then
+    channel_id_sequence := pg_get_serial_sequence('crm_channel_expansion', 'id')::regclass;
+  end if;
+
+  if sales_id_sequence is not null then
+    execute format('alter sequence %s no cycle', sales_id_sequence);
+  end if;
+  if channel_id_sequence is not null then
+    execute format('alter sequence %s no cycle', channel_id_sequence);
+  end if;
+end $$;
+
+-- ALTER SEQUENCE RESTART is transactional; setval() would survive a rollback.
+do $$
+declare
+  customer_code_next bigint;
+  opportunity_code_next bigint;
+begin
+  select greatest(
+    coalesce((
+      select max(substring(customer_code from '([0-9]+)$')::bigint) + 1
+      from crm_customer
+      where customer_code ~ '^CUS-[0-9]{8}-[0-9]+$'
+    ), 1),
+    (select case when is_called then last_value + 1 else last_value end from crm_customer_code_seq)
+  )
+  into customer_code_next;
+
+  select greatest(
+    coalesce((
+      select max(substring(opportunity_code from '([0-9]+)$')::bigint) + 1
+      from crm_opportunity
+      where opportunity_code ~ '^OPP-[0-9]{8}-[0-9]+$'
+    ), 1),
+    (select case when is_called then last_value + 1 else last_value end from crm_opportunity_code_seq)
+  )
+  into opportunity_code_next;
+
+  execute format('alter sequence crm_customer_code_seq restart with %s', customer_code_next);
+  execute format('alter sequence crm_opportunity_code_seq restart with %s', opportunity_code_next);
+end $$;
+
+do $$
+declare
+  sales_id_sequence regclass;
+  channel_id_sequence regclass;
+  sales_id_max bigint;
+  sales_id_current_next bigint;
+  channel_id_max bigint;
+  channel_id_current_next bigint;
+begin
+  if to_regclass('crm_sales_expansion') is not null then
+    sales_id_sequence := pg_get_serial_sequence('crm_sales_expansion', 'id')::regclass;
+  end if;
+  if to_regclass('crm_channel_expansion') is not null then
+    channel_id_sequence := pg_get_serial_sequence('crm_channel_expansion', 'id')::regclass;
+  end if;
+
+  if sales_id_sequence is not null then
+    select coalesce(max(id), 0) into sales_id_max from crm_sales_expansion;
+    if sales_id_max = 9223372036854775807 then
+      raise exception 'crm_sales_expansion identity sequence has exhausted bigint values';
+    end if;
+    execute format(
+      'select case when is_called then last_value + 1 else last_value end from %s',
+      sales_id_sequence
+    ) into sales_id_current_next;
+    execute format(
+      'alter sequence %s restart with %s',
+      sales_id_sequence,
+      greatest(sales_id_max + 1, sales_id_current_next)
+    );
+  end if;
+
+  if channel_id_sequence is not null then
+    select coalesce(max(id), 0) into channel_id_max from crm_channel_expansion;
+    if channel_id_max = 9223372036854775807 then
+      raise exception 'crm_channel_expansion identity sequence has exhausted bigint values';
+    end if;
+    execute format(
+      'select case when is_called then last_value + 1 else last_value end from %s',
+      channel_id_sequence
+    ) into channel_id_current_next;
+    execute format(
+      'alter sequence %s restart with %s',
+      channel_id_sequence,
+      greatest(channel_id_max + 1, channel_id_current_next)
+    );
+  end if;
+end $$;
+
+commit;
diff --git a/sql/init_full_pg17.sql b/sql/init_full_pg17.sql
index 33b310b9..95e9e5e2 100644
--- a/sql/init_full_pg17.sql
+++ b/sql/init_full_pg17.sql
@@ -117,6 +117,8 @@ create table if not exists crm_customer (
   constraint uk_crm_customer_code unique (customer_code)
 );
 
+create sequence if not exists crm_customer_code_seq start with 1 increment by 1 minvalue 1;
+
 create table if not exists crm_opportunity (
   id bigint generated by default as identity primary key,
   opportunity_code varchar(50) not null,
@@ -156,6 +158,8 @@ create table if not exists crm_opportunity (
   constraint fk_crm_opportunity_customer foreign key (customer_id) references crm_customer(id)
 );
 
+create sequence if not exists crm_opportunity_code_seq start with 1 increment by 1 minvalue 1;
+
 create table if not exists crm_opportunity_followup (
   id bigint generated by default as identity primary key,
   opportunity_id bigint not null,
