diff --git a/backend/src/main/java/com/xly/erp/common/exception/BusinessException.java b/backend/src/main/java/com/xly/erp/common/exception/BusinessException.java
new file mode 100644
index 0000000..4059c4a
--- /dev/null
+++ b/backend/src/main/java/com/xly/erp/common/exception/BusinessException.java
@@ -0,0 +1,29 @@
+package com.xly.erp.common.exception;
+
+import com.xly.erp.common.response.ResultCode;
+
+/**
+ * 业务异常(docs/04 § 1.5 SSoT)。
+ *
+ *
REQ-USR-001 T2。业务错误统一抛本异常,由 {@link GlobalExceptionHandler} 捕获转 Result。
+ */
+public class BusinessException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ private final ResultCode resultCode;
+
+ public BusinessException(ResultCode resultCode) {
+ super(resultCode.getMessage());
+ this.resultCode = resultCode;
+ }
+
+ public BusinessException(ResultCode resultCode, String message) {
+ super(message);
+ this.resultCode = resultCode;
+ }
+
+ public ResultCode getResultCode() {
+ return resultCode;
+ }
+}
diff --git a/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..327a87c
--- /dev/null
+++ b/backend/src/main/java/com/xly/erp/common/exception/GlobalExceptionHandler.java
@@ -0,0 +1,60 @@
+package com.xly.erp.common.exception;
+
+import com.xly.erp.common.response.Result;
+import com.xly.erp.common.response.ResultCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 全局异常处理(docs/04 § 1.5 SSoT)。
+ *
+ * REQ-USR-001 T2:把 BusinessException / 参数校验失败 / 唯一键冲突 / 未捕获异常
+ * 统一转为 {@link Result},失败响应不抛栈到前端。
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ /**
+ * 业务异常 → 对应错误码。
+ */
+ @ExceptionHandler(BusinessException.class)
+ public Result handleBusinessException(BusinessException ex) {
+ return Result.fail(ex.getResultCode(), ex.getMessage());
+ }
+
+ /**
+ * Bean Validation(@Valid)失败 → 40001,message 取首个字段错误提示。
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Result handleValidationException(MethodArgumentNotValidException ex) {
+ FieldError fieldError = ex.getBindingResult().getFieldError();
+ String message = fieldError != null
+ ? fieldError.getField() + ": " + fieldError.getDefaultMessage()
+ : ResultCode.PARAM_INVALID.getMessage();
+ return Result.fail(ResultCode.PARAM_INVALID, message);
+ }
+
+ /**
+ * 唯一键冲突(并发兜底)→ 40901 用户名已存在。
+ */
+ @ExceptionHandler(DuplicateKeyException.class)
+ public Result handleDuplicateKeyException(DuplicateKeyException ex) {
+ return Result.fail(ResultCode.USERNAME_EXISTS, ResultCode.USERNAME_EXISTS.getMessage());
+ }
+
+ /**
+ * 兜底:未捕获异常记录 ERROR 日志(含栈),对外只返回通用错误码,不泄露内部细节。
+ */
+ @ExceptionHandler(Exception.class)
+ public Result handleException(Exception ex) {
+ LOG.error("系统异常", ex);
+ return Result.fail(ResultCode.SYSTEM_ERROR, ResultCode.SYSTEM_ERROR.getMessage());
+ }
+}
diff --git a/backend/src/main/java/com/xly/erp/common/response/Result.java b/backend/src/main/java/com/xly/erp/common/response/Result.java
new file mode 100644
index 0000000..37930c4
--- /dev/null
+++ b/backend/src/main/java/com/xly/erp/common/response/Result.java
@@ -0,0 +1,73 @@
+package com.xly.erp.common.response;
+
+import java.io.Serializable;
+
+/**
+ * 统一响应体(docs/04 § 1.4 SSoT)。
+ *
+ * REQ-USR-001 T2。所有接口返回 {@code Result}:{@code code=0} 成功,非 0 为错误码。
+ *
+ * @param 业务数据类型
+ */
+public class Result implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private int code;
+ private String message;
+ private T data;
+
+ public Result() {
+ }
+
+ public Result(int code, String message, T data) {
+ this.code = code;
+ this.message = message;
+ this.data = data;
+ }
+
+ /**
+ * 成功且携带数据。
+ */
+ public static Result success(T data) {
+ return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
+ }
+
+ /**
+ * 成功且不携带数据。
+ */
+ public static Result success() {
+ return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
+ }
+
+ /**
+ * 失败,携带错误码与可读提示。
+ */
+ public static Result fail(ResultCode code, String message) {
+ return new Result<>(code.getCode(), message, null);
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public T getData() {
+ return data;
+ }
+
+ public void setData(T data) {
+ this.data = data;
+ }
+}
diff --git a/backend/src/main/java/com/xly/erp/common/response/ResultCode.java b/backend/src/main/java/com/xly/erp/common/response/ResultCode.java
new file mode 100644
index 0000000..ae9d3ec
--- /dev/null
+++ b/backend/src/main/java/com/xly/erp/common/response/ResultCode.java
@@ -0,0 +1,45 @@
+package com.xly.erp.common.response;
+
+/**
+ * 统一错误码枚举(docs/05 / spec § 6 SSoT)。
+ *
+ * REQ-USR-001 T2:一次性建好本枚举,含本 REQ 使用的 0/40001/40301/40901,
+ * 并预留后续 REQ 复用的 40101/40302/40401/42201,避免后续重复修改公共文件。
+ */
+public enum ResultCode {
+
+ /** 成功。 */
+ SUCCESS(0, "success"),
+ /** 参数校验失败(字段格式 / 必填 / 枚举越界 / 关联 id 不存在)。 */
+ PARAM_INVALID(40001, "参数校验失败"),
+ /** 认证失败(用户名或密码错误;预留 REQ-USR-004)。 */
+ UNAUTHORIZED(40101, "认证失败"),
+ /** 无权限(非管理员调用)。 */
+ FORBIDDEN(40301, "无权限"),
+ /** 账号已禁用(预留 REQ-USR-004)。 */
+ ACCOUNT_DISABLED(40302, "账号已禁用"),
+ /** 资源不存在(预留 REQ-USR-002)。 */
+ NOT_FOUND(40401, "资源不存在"),
+ /** 用户名已存在(sUserName 全局唯一冲突)。 */
+ USERNAME_EXISTS(40901, "用户名已存在"),
+ /** 分页参数非法(预留 REQ-USR-003)。 */
+ PAGE_PARAM_INVALID(42201, "分页参数非法"),
+ /** 系统内部错误(兜底)。 */
+ SYSTEM_ERROR(50000, "系统繁忙,请稍后重试");
+
+ private final int code;
+ private final String message;
+
+ ResultCode(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java b/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java
new file mode 100644
index 0000000..0a5ae89
--- /dev/null
+++ b/backend/src/test/java/com/xly/erp/common/exception/GlobalExceptionHandlerTest.java
@@ -0,0 +1,23 @@
+package com.xly.erp.common.exception;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.xly.erp.common.response.Result;
+import com.xly.erp.common.response.ResultCode;
+import org.junit.jupiter.api.Test;
+
+/**
+ * REQ-USR-001 T2:全局异常处理器把 BusinessException 转为统一 Result。
+ */
+class GlobalExceptionHandlerTest {
+
+ private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
+
+ @Test
+ void businessExceptionMapsToResult() {
+ BusinessException ex = new BusinessException(ResultCode.USERNAME_EXISTS, "用户名已存在");
+ Result r = handler.handleBusinessException(ex);
+ assertThat(r.getCode()).isEqualTo(40901);
+ assertThat(r.getMessage()).isEqualTo("用户名已存在");
+ }
+}
diff --git a/backend/src/test/java/com/xly/erp/common/response/ResultTest.java b/backend/src/test/java/com/xly/erp/common/response/ResultTest.java
new file mode 100644
index 0000000..f0a7469
--- /dev/null
+++ b/backend/src/test/java/com/xly/erp/common/response/ResultTest.java
@@ -0,0 +1,26 @@
+package com.xly.erp.common.response;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * REQ-USR-001 T2:统一响应体 Result 行为校验。
+ */
+class ResultTest {
+
+ @Test
+ void successCarriesCodeZeroAndData() {
+ Result r = Result.success("hello");
+ assertThat(r.getCode()).isEqualTo(0);
+ assertThat(r.getData()).isEqualTo("hello");
+ }
+
+ @Test
+ void failCarriesBusinessCodeAndMessage() {
+ Result r = Result.fail(ResultCode.USERNAME_EXISTS, "用户名已存在");
+ assertThat(r.getCode()).isEqualTo(40901);
+ assertThat(r.getMessage()).isEqualTo("用户名已存在");
+ assertThat(r.getData()).isNull();
+ }
+}