diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 0000000..ea867cc
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,136 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.5
+
+
+
+ com.example
+ erp
+ 0.0.1-SNAPSHOT
+ erp
+ 小羚羊 ERP 后端
+ jar
+
+
+ 21
+ 3.5.7
+ 0.12.6
+ 5.8.28
+ 10.17.0
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ org.flywaydb
+ flyway-core
+ ${flyway.version}
+
+
+ org.flywaydb
+ flyway-mysql
+ ${flyway.version}
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ cn.hutool
+ hutool-all
+ ${hutool.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/backend/src/main/java/com/example/erp/Application.java b/backend/src/main/java/com/example/erp/Application.java
new file mode 100644
index 0000000..5748201
--- /dev/null
+++ b/backend/src/main/java/com/example/erp/Application.java
@@ -0,0 +1,12 @@
+package com.example.erp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@SpringBootApplication
+public class Application {
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..a58b22c
--- /dev/null
+++ b/backend/src/main/resources/application-dev.yml
@@ -0,0 +1,4 @@
+logging:
+ level:
+ com.example.erp: DEBUG
+ org.springframework.security: DEBUG
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
new file mode 100644
index 0000000..dd5f5b6
--- /dev/null
+++ b/backend/src/main/resources/application.yml
@@ -0,0 +1,27 @@
+spring:
+ datasource:
+ url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
+ username: ${DB_USER}
+ password: ${DB_PASSWORD}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ flyway:
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+ baseline-version: 1
+ validate-on-migrate: false
+ out-of-order: false
+
+server:
+ port: 8080
+
+jwt:
+ secret: ${JWT_SECRET}
+ access-token-expiry: 86400
+ refresh-token-expiry: 604800
+
+mybatis-plus:
+ configuration:
+ map-underscore-to-camel-case: false
+ global-config:
+ db-config:
+ id-type: auto
diff --git a/backend/src/main/resources/db/migration/V1__initial_schema.sql b/backend/src/main/resources/db/migration/V1__initial_schema.sql
new file mode 100644
index 0000000..6f7ef83
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V1__initial_schema.sql
@@ -0,0 +1,151 @@
+-- Flyway migration V1 — initial schema for 小羚羊
+-- Generated: 2026-05-08T01:01:55Z
+-- Source: 由 A4 db-init 从 docs/03-数据库设计文档.md 翻译生成(schema SSoT 是 docs/03)
+-- This is the FIRST migration; subsequent schema changes must be written as new files sql/migrations/V2__.sql, V3__... etc.
+-- Apply: Flyway runs this automatically at Spring Boot startup.
+-- Do not hand-edit this file after it is committed; write a new migration instead.
+
+SET NAMES utf8mb4;
+SET CHARACTER_SET_CLIENT = utf8mb4;
+
+-- ============================================================
+-- Table: usr_user
+-- ============================================================
+CREATE TABLE `usr_user` (
+ `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
+ `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
+ `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
+ `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
+ `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
+ `sUserCode` VARCHAR(50) NOT NULL COMMENT '用户号(业务编号,人类可读唯一标识)',
+ `sUsername` VARCHAR(100) NOT NULL COMMENT '用户名(登录标识,全局唯一,不可修改)',
+ `sPasswordHash` VARCHAR(255) NOT NULL COMMENT 'BCrypt 哈希密码,禁止存储明文',
+ `sUserType` VARCHAR(20) NOT NULL DEFAULT '普通用户' COMMENT '用户类型:普通用户 / 超级管理员',
+ `sLanguage` VARCHAR(20) NOT NULL DEFAULT '中文' COMMENT '界面语言:中文 / 英文 / 繁体',
+ `bCanEditDoc` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '单据修改权限:0=否,1=是',
+ `bIsDisabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否作废/禁用:0=正常,1=禁用',
+ `sEmployeeId` VARCHAR(100) NULL COMMENT '关联职员 ID(跨模块引用,职员未关联时为 NULL)',
+ `sCreatorUsername` VARCHAR(100) NULL COMMENT '制单人用户名(冗余字段,便于列表展示)',
+ `tLastLoginDate` DATETIME NULL COMMENT '最后登录时间',
+ `iLoginFailCount` INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数,用于防暴力破解',
+ `tLockUntil` DATETIME NULL COMMENT '账号锁定截止时间,NULL 表示未锁定',
+ PRIMARY KEY (`iIncrement`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='用户账户主表,存储登录信息、类型、语言偏好及安全控制字段';
+
+-- ============================================================
+-- Table: usr_permission_group
+-- ============================================================
+CREATE TABLE `usr_permission_group` (
+ `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
+ `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
+ `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
+ `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
+ `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
+ `sGroupCode` VARCHAR(100) NOT NULL COMMENT '权限代码(如 usr:create、usr:edit),全局唯一',
+ `sGroupName` VARCHAR(200) NOT NULL COMMENT '权限显示名称(如"新增用户"、"修改用户")',
+ `sCategory` VARCHAR(100) NULL COMMENT '权限分类标签,用于前端权限分组展示',
+ PRIMARY KEY (`iIncrement`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='权限分类/权限组定义表,每行对应一个可分配给用户的权限项';
+
+-- ============================================================
+-- Table: usr_user_permission
+-- ============================================================
+CREATE TABLE `usr_user_permission` (
+ `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
+ `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
+ `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
+ `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
+ `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
+ `sUserId` VARCHAR(100) NOT NULL COMMENT '关联 usr_user.sId',
+ `sPermGroupId` VARCHAR(100) NOT NULL COMMENT '关联 usr_permission_group.sId',
+ PRIMARY KEY (`iIncrement`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='用户与权限组的多对多关联表';
+
+-- ============================================================
+-- Table: tStaff
+-- ============================================================
+CREATE TABLE `tStaff` (
+ `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
+ `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
+ `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
+ `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
+ `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
+ `sStaffNo` VARCHAR(50) NULL COMMENT '职员编号;系统内唯一',
+ `sStaffName` VARCHAR(50) NOT NULL COMMENT '职员姓名',
+ `sDepartment` VARCHAR(100) NULL DEFAULT NULL COMMENT '所属部门(本期暂用字符串,未来如需独立 tDepartment 字典表再另行重构)',
+ `sCreatedBy` VARCHAR(50) NULL COMMENT '制单人',
+ `bDeleted` BIT(1) NOT NULL DEFAULT 0 COMMENT '软删除标记',
+ `tDeletedDate` DATETIME NULL DEFAULT NULL COMMENT '软删除时间',
+ `sDeletedBy` VARCHAR(50) NULL DEFAULT NULL COMMENT '软删除操作人',
+ PRIMARY KEY (`iIncrement`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='职员维度(员工名 / 部门 / 编号)';
+
+-- ============================================================
+-- Table: brand
+-- ============================================================
+CREATE TABLE `brand` (
+ `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
+ `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
+ `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
+ `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
+ `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
+ `sName` VARCHAR(100) NULL COMMENT '公司名称',
+ `sShortName` VARCHAR(100) NULL COMMENT '公司简称',
+ `sNo` VARCHAR(100) NULL COMMENT '单位编号(登录账号根据单位编号作为前缀)',
+ PRIMARY KEY (`iIncrement`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ COMMENT='公司表';
+
+-- ============================================================
+-- Indexes: usr_user
+-- ============================================================
+CREATE UNIQUE INDEX `uk_usr_user_sid` ON `usr_user` (`sId`);
+CREATE UNIQUE INDEX `uk_usr_user_username` ON `usr_user` (`sUsername`);
+CREATE UNIQUE INDEX `uk_usr_user_usercode` ON `usr_user` (`sUserCode`);
+CREATE INDEX `idx_usr_user_tenant` ON `usr_user` (`sBrandsId`, `sSubsidiaryId`);
+CREATE INDEX `idx_usr_user_type` ON `usr_user` (`sUserType`);
+CREATE INDEX `idx_usr_user_disabled` ON `usr_user` (`bIsDisabled`);
+
+-- ============================================================
+-- Indexes: usr_permission_group
+-- ============================================================
+CREATE UNIQUE INDEX `uk_usr_perm_group_sid` ON `usr_permission_group` (`sId`);
+CREATE UNIQUE INDEX `uk_usr_perm_group_code` ON `usr_permission_group` (`sGroupCode`);
+CREATE INDEX `idx_usr_perm_group_tenant` ON `usr_permission_group` (`sBrandsId`, `sSubsidiaryId`);
+
+-- ============================================================
+-- Indexes: usr_user_permission
+-- ============================================================
+CREATE UNIQUE INDEX `uk_usr_user_perm` ON `usr_user_permission` (`sUserId`, `sPermGroupId`);
+CREATE INDEX `idx_usr_user_perm_user` ON `usr_user_permission` (`sUserId`);
+CREATE INDEX `idx_usr_user_perm_group` ON `usr_user_permission` (`sPermGroupId`);
+
+-- ============================================================
+-- Indexes: tStaff
+-- ============================================================
+CREATE UNIQUE INDEX `uk_staff_no` ON `tStaff` (`sStaffNo`);
+CREATE INDEX `idx_staff_name` ON `tStaff` (`sStaffName`);
+CREATE INDEX `idx_department` ON `tStaff` (`sDepartment`);
+
+-- ============================================================
+-- Indexes: brand
+-- ============================================================
+CREATE UNIQUE INDEX `uk_brand_no` ON `brand` (`sNo`);
+CREATE INDEX `idx_brand_name` ON `brand` (`sName`);
+
+-- ============================================================
+-- Foreign Keys
+-- ============================================================
+ALTER TABLE `usr_user_permission`
+ ADD CONSTRAINT `fk_usr_user_perm_user`
+ FOREIGN KEY (`sUserId`) REFERENCES `usr_user` (`sId`)
+ ON DELETE CASCADE ON UPDATE CASCADE;
+
+ALTER TABLE `usr_user_permission`
+ ADD CONSTRAINT `fk_usr_user_perm_group`
+ FOREIGN KEY (`sPermGroupId`) REFERENCES `usr_permission_group` (`sId`)
+ ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/backend/src/test/java/com/example/erp/ApplicationContextTest.java b/backend/src/test/java/com/example/erp/ApplicationContextTest.java
new file mode 100644
index 0000000..88fabbd
--- /dev/null
+++ b/backend/src/test/java/com/example/erp/ApplicationContextTest.java
@@ -0,0 +1,14 @@
+package com.example.erp;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("test")
+class ApplicationContextTest {
+
+ @Test
+ void contextLoads() {
+ }
+}
diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml
new file mode 100644
index 0000000..b301682
--- /dev/null
+++ b/backend/src/test/resources/application-test.yml
@@ -0,0 +1,19 @@
+spring:
+ datasource:
+ url: jdbc:mysql://${DB_HOST:118.178.19.35}:${DB_PORT:3318}/${DB_SCHEMA:xlyweberp_vibe_erp_test}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
+ username: ${DB_USER:xlyprint}
+ password: ${DB_PASSWORD:xlyXLYprint2016}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ flyway:
+ baseline-on-migrate: true
+ baseline-version: 1
+ validate-on-migrate: false
+
+jwt:
+ secret: ${JWT_SECRET:a3b7e8f1c4d6029e5b8f37a1c9d2e4068b5f1d3a7c0e9b2f48d6a1c5e7f9b3d2}
+ access-token-expiry: 86400
+ refresh-token-expiry: 604800
+
+logging:
+ level:
+ com.example.erp: DEBUG