Compare commits

...

2 Commits

Author SHA1 Message Date
文睿 张
855642ea75 已完成迁移体系清理和 Docker 初始化改造:
- 新版 [init.sql](C:\\1111\\12.project\\VSCode\\warehouse-mms\\scripts\\init.sql) 直接创建最新空库结构、默认组织/权限/仓库/分类。
- [docker-compose.yml](C:\\1111\\12.project\\VSCode\\warehouse-mms\\docker-compose.yml) 改为由 MySQL 在空数据卷首次启动时自动执行 init.sql。
- [entrypoint.sh](C:\\1111\\12.project\\VSCode\\warehouse-mms\\scripts\\entrypoint.sh) 不再重复执行迁移。
- 已删除 scripts/migrations/、pkg/migration/、cmd/migrate/ 和旧迁移说明。
- 已执行 docker compose down -v --remove-orphans;输出确认删除了 warehouse-mms_uploads_data 卷。
- go test ./... 全部通过;git diff --check 与 Compose 配置检查通过。
2026-09-17 16:22:52 +08:00
文睿 张
8cafca4b44 已完成迁移体系清理和 Docker 初始化改造:
- 新版 [init.sql](C:\\1111\\12.project\\VSCode\\warehouse-mms\\scripts\\init.sql) 直接创建最新空库结构、默认组织/权限/仓库/分类。
- [docker-compose.yml](C:\\1111\\12.project\\VSCode\\warehouse-mms\\docker-compose.yml) 改为由 MySQL 在空数据卷首次启动时自动执行 init.sql。
- [entrypoint.sh](C:\\1111\\12.project\\VSCode\\warehouse-mms\\scripts\\entrypoint.sh) 不再重复执行迁移。
- 已删除 scripts/migrations/、pkg/migration/、cmd/migrate/ 和旧迁移说明。
- 已执行 docker compose down -v --remove-orphans;输出确认删除了 warehouse-mms_uploads_data 卷。
2026-09-17 16:21:53 +08:00
19 changed files with 46 additions and 1285 deletions

View File

@ -1,34 +0,0 @@
package main
import (
"context"
"database/sql"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/wmms/wmms/pkg/migration"
)
func main() {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
log.Fatal("DB_DSN is required")
}
dir := os.Getenv("MIGRATIONS_DIR")
if dir == "" {
dir = "scripts/migrations"
}
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("open database: %v", err)
}
defer db.Close()
if err := db.PingContext(context.Background()); err != nil {
log.Fatalf("connect database: %v", err)
}
if err := migration.Run(context.Background(), db, dir); err != nil {
log.Fatalf("run migrations: %v", err)
}
log.Print("migrations applied successfully")
}

View File

@ -11,6 +11,7 @@ services:
- "3307:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./scripts/init.sql:/docker-entrypoint-initdb.d/001-init.sql:ro
command: >
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
@ -35,7 +36,6 @@ services:
GIN_MODE: release
volumes:
- uploads_data:/app/uploads
- ./scripts/migrations:/docker-entrypoint-initdb.d/migrations:ro
depends_on:
db:
condition: service_healthy

View File

@ -1,31 +0,0 @@
# 第一阶段数据库迁移
本阶段将公司组织、业务伙伴、仓库/库位、物料基础配置和库存余额拆分到独立表。旧的
`materials.current_stock` 与 `materials.warning_stock` 暂时保留用于回滚观察,但应用代码不再读写。
## 执行顺序
1. 备份现有数据库,并暂停入库、出库和物料申请操作。
2. 为当前 shell 设置 `DB_DSN`,不要把账号密码写入仓库。
3. 在项目根目录运行:
```powershell
go run ./cmd/migrate
```
4. 确认迁移 `001` 到 `006` 均为非 dirty 状态,并检查 `006` 最后的库存差异查询没有结果。
5. 发布后端和前端,再恢复库存操作。
迁移器按文件名顺序执行并记录在 `schema_migrations`。MySQL DDL 可能隐式提交;如果某个版本为
dirty,迁移器会停止,必须根据报错的语句编号人工核对并修复,不能直接假定事务已回滚。
## 核对重点
- 每个旧用户均已分配默认组织和默认仓库。
- 旧物料数量与 `inventory_balances.on_hand` 汇总一致。
- 旧预警值与 `stock_warning_rules.minimum_available` 一致。
- `materials` API 响应不再包含库存数量和预警值。
- 公司组织位于 `organizations`,外部公司位于 `business_partners`,合作关系位于 `organization_partners`。
不要在生产库直接试跑。应先在数据库副本验证一次;当前自动化测试只验证迁移执行器和应用查询,
不替代生产数据副本的迁移核对。

View File

@ -1,202 +0,0 @@
package migration
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
const createMigrationsTable = `CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(100) PRIMARY KEY,
dirty BOOLEAN NOT NULL DEFAULT TRUE,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`
// Run applies .sql files in lexical filename order.
// MySQL 5.7 may implicitly commit DDL, so every migration is marked dirty
// before its first statement and only marked clean after all statements pass.
func Run(ctx context.Context, db *sql.DB, dir string) error {
if db == nil {
return fmt.Errorf("migration database is nil")
}
if _, err := db.ExecContext(ctx, createMigrationsTable); err != nil {
return fmt.Errorf("create schema_migrations: %w", err)
}
var dirtyVersion string
err := db.QueryRowContext(ctx, `SELECT version FROM schema_migrations WHERE dirty = TRUE ORDER BY applied_at DESC LIMIT 1`).Scan(&dirtyVersion)
if err == nil {
return fmt.Errorf("migration %s is dirty; stop and perform manual repair before retrying", dirtyVersion)
}
if err != sql.ErrNoRows {
return fmt.Errorf("check dirty migration: %w", err)
}
applied, err := loadApplied(ctx, db)
if err != nil {
return err
}
files, err := migrationFiles(dir)
if err != nil {
return err
}
for _, path := range files {
version := filepath.Base(path)
if applied[version] {
continue
}
contents, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read migration %s: %w", version, err)
}
statements, err := splitStatements(string(contents))
if err != nil {
return fmt.Errorf("parse migration %s: %w", version, err)
}
if _, err := db.ExecContext(ctx, `INSERT INTO schema_migrations (version, dirty) VALUES (?, TRUE)`, version); err != nil {
return fmt.Errorf("mark migration %s dirty: %w", version, err)
}
for index, statement := range statements {
if _, err := db.ExecContext(ctx, statement); err != nil {
return fmt.Errorf("migration %s statement %d failed: %w; version remains dirty because MySQL DDL cannot be transactionally rolled back; perform manual repair", version, index+1, err)
}
}
if _, err := db.ExecContext(ctx, `UPDATE schema_migrations SET dirty = FALSE, applied_at = CURRENT_TIMESTAMP WHERE version = ?`, version); err != nil {
return fmt.Errorf("mark migration %s clean: %w; version remains dirty and requires manual repair", version, err)
}
}
return nil
}
func loadApplied(ctx context.Context, db *sql.DB) (map[string]bool, error) {
rows, err := db.QueryContext(ctx, `SELECT version FROM schema_migrations WHERE dirty = FALSE`)
if err != nil {
return nil, fmt.Errorf("list applied migrations: %w", err)
}
defer rows.Close()
applied := make(map[string]bool)
for rows.Next() {
var version string
if err := rows.Scan(&version); err != nil {
return nil, fmt.Errorf("scan applied migration: %w", err)
}
applied[version] = true
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate applied migrations: %w", err)
}
return applied, nil
}
func migrationFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("read migrations directory %s: %w", dir, err)
}
files := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || strings.ToLower(filepath.Ext(entry.Name())) != ".sql" {
continue
}
files = append(files, filepath.Join(dir, entry.Name()))
}
sort.Strings(files)
return files, nil
}
func splitStatements(input string) ([]string, error) {
var statements []string
var current strings.Builder
var quote rune
escaped := false
lineComment := false
blockComment := false
runes := []rune(input)
flush := func() {
statement := strings.TrimSpace(current.String())
if statement != "" {
statements = append(statements, statement)
}
current.Reset()
}
for i := 0; i < len(runes); i++ {
ch := runes[i]
next := rune(0)
if i+1 < len(runes) {
next = runes[i+1]
}
if lineComment {
if ch == '\n' {
lineComment = false
if current.Len() > 0 {
current.WriteRune(' ')
}
}
continue
}
if blockComment {
if ch == '*' && next == '/' {
blockComment = false
i++
if current.Len() > 0 {
current.WriteRune(' ')
}
}
continue
}
if quote != 0 {
current.WriteRune(ch)
if escaped {
escaped = false
continue
}
if ch == '\\' {
escaped = true
continue
}
if ch == quote {
quote = 0
}
continue
}
if ch == '-' && next == '-' {
lineComment = true
i++
continue
}
if ch == '#' {
lineComment = true
continue
}
if ch == '/' && next == '*' {
blockComment = true
i++
continue
}
if ch == '\'' || ch == '"' || ch == '`' {
quote = ch
current.WriteRune(ch)
continue
}
if ch == ';' {
flush()
continue
}
current.WriteRune(ch)
}
if quote != 0 {
return nil, fmt.Errorf("unterminated quoted string")
}
if blockComment {
return nil, fmt.Errorf("unterminated block comment")
}
flush()
return statements, nil
}

View File

@ -1,136 +0,0 @@
package migration
import (
"context"
"errors"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
const createMigrationsTableSQL = `CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(100) PRIMARY KEY,
dirty BOOLEAN NOT NULL DEFAULT TRUE,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`
func writeMigration(t *testing.T, name, sqlText string) string {
t.Helper()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, name), []byte(sqlText), 0o600); err != nil {
t.Fatalf("write migration: %v", err)
}
return dir
}
func expectCleanState(mock sqlmock.Sqlmock, applied ...string) {
mock.ExpectExec(regexp.QuoteMeta(createMigrationsTableSQL)).WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = TRUE ORDER BY applied_at DESC LIMIT 1`)).
WillReturnRows(sqlmock.NewRows([]string{"version"}))
rows := sqlmock.NewRows([]string{"version"})
for _, version := range applied {
rows.AddRow(version)
}
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = FALSE`)).WillReturnRows(rows)
}
func TestRunAppliesEachMigrationOnce(t *testing.T) {
dir := writeMigration(t, "001_create_widgets.sql", `CREATE TABLE widgets (id BIGINT PRIMARY KEY);`)
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("open sqlmock: %v", err)
}
t.Cleanup(func() { db.Close() })
expectCleanState(mock)
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO schema_migrations (version, dirty) VALUES (?, TRUE)`)).
WithArgs("001_create_widgets.sql").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta(`CREATE TABLE widgets (id BIGINT PRIMARY KEY)`)).
WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta(`UPDATE schema_migrations SET dirty = FALSE, applied_at = CURRENT_TIMESTAMP WHERE version = ?`)).
WithArgs("001_create_widgets.sql").WillReturnResult(sqlmock.NewResult(0, 1))
if err := Run(context.Background(), db, dir); err != nil {
t.Fatalf("first run: %v", err)
}
expectCleanState(mock, "001_create_widgets.sql")
if err := Run(context.Background(), db, dir); err != nil {
t.Fatalf("second run: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRunLeavesFailedMigrationDirty(t *testing.T) {
dir := writeMigration(t, "002_broken.sql", `CREATE TABLE broken (id BIGINT); ALTER TABLE missing ADD value INT;`)
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("open sqlmock: %v", err)
}
t.Cleanup(func() { db.Close() })
expectCleanState(mock)
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO schema_migrations (version, dirty) VALUES (?, TRUE)`)).
WithArgs("002_broken.sql").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta(`CREATE TABLE broken (id BIGINT)`)).WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta(`ALTER TABLE missing ADD value INT`)).WillReturnError(errors.New("missing table"))
err = Run(context.Background(), db, dir)
if err == nil {
t.Fatal("expected migration failure")
}
for _, fragment := range []string{"002_broken.sql", "statement 2", "dirty", "manual repair"} {
if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(fragment)) {
t.Fatalf("error %q does not contain %q", err, fragment)
}
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestRunStopsWhenDirtyMigrationExists(t *testing.T) {
dir := writeMigration(t, "003_should_not_run.sql", `CREATE TABLE should_not_run (id BIGINT);`)
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("open sqlmock: %v", err)
}
t.Cleanup(func() { db.Close() })
mock.ExpectExec(regexp.QuoteMeta(createMigrationsTableSQL)).WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = TRUE ORDER BY applied_at DESC LIMIT 1`)).
WillReturnRows(sqlmock.NewRows([]string{"version"}).AddRow("002_broken.sql"))
err = Run(context.Background(), db, dir)
if err == nil || !strings.Contains(err.Error(), "002_broken.sql") {
t.Fatalf("expected dirty migration error, got %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestSplitStatementsPreservesSemicolonsInsideStrings(t *testing.T) {
statements, err := splitStatements("INSERT INTO notes(value) VALUES ('a;b'); -- comment\nUPDATE notes SET value = \"c;d\";")
if err != nil {
t.Fatalf("split statements: %v", err)
}
want := []string{
"INSERT INTO notes(value) VALUES ('a;b')",
"UPDATE notes SET value = \"c;d\"",
}
if len(statements) != len(want) {
t.Fatalf("got %d statements, want %d: %#v", len(statements), len(want), statements)
}
for i := range want {
if statements[i] != want[i] {
t.Fatalf("statement %d = %q, want %q", i+1, statements[i], want[i])
}
}
}

View File

@ -7,11 +7,5 @@ until mysqladmin ping -h db -uroot -pwmms1234 --silent 2>/dev/null; do
done
echo "MySQL is ready."
echo "Running database migrations..."
for f in $(ls /docker-entrypoint-initdb.d/migrations/*.sql 2>/dev/null | sort); do
echo "Executing: $f"
mysql -h db -uroot -pwmms1234 wmms < "$f" 2>/dev/null && echo "OK: $(basename $f)" || echo "FAILED: $(basename $f)"
done
echo "Starting application..."
exec "$@"

View File

@ -1,237 +1,48 @@
-- ============================================
-- Warehouse Material Management System (WMMS)
-- Database Initialization Script
-- MySQL 5.7+
-- ============================================
-- Create database
CREATE DATABASE IF NOT EXISTS wmms;
-- WMMS fresh schema for MySQL 8.0. Executed once for an empty Docker volume.
CREATE DATABASE IF NOT EXISTS wmms CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE wmms;
-- ============================================
-- Table: materials
-- ============================================
CREATE TABLE IF NOT EXISTS materials (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
material_code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
material_image VARCHAR(500),
spec VARCHAR(200),
category VARCHAR(100),
unit VARCHAR(20),
warning_stock INT DEFAULT 0,
current_stock INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_material_code (material_code),
INDEX idx_category (category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE users (id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, description VARCHAR(500) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE organizations (id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', default_timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE roles (id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(80) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE permissions (id BIGINT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(120) NOT NULL UNIQUE, name VARCHAR(150) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE role_permissions (role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(role_id,permission_id), FOREIGN KEY(role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY(permission_id) REFERENCES permissions(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_roles (user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(user_id,role_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(role_id) REFERENCES roles(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_organizations (user_id BIGINT NOT NULL, organization_id BIGINT NOT NULL, is_default BOOLEAN NOT NULL DEFAULT FALSE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(user_id,organization_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE revoked_tokens (jti VARCHAR(64) PRIMARY KEY, user_id BIGINT NOT NULL, expires_at DATETIME NOT NULL, revoked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_revoked_tokens_expires_at(expires_at), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_types (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, code VARCHAR(50) NOT NULL, name VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', UNIQUE KEY uk_material_types_org_code(organization_id,code), FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_categories (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, code VARCHAR(50) NOT NULL, name VARCHAR(100) NOT NULL, parent_id BIGINT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', UNIQUE KEY uk_material_categories_org_code(organization_id,code), FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT, FOREIGN KEY(parent_id) REFERENCES material_categories(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE units (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, code VARCHAR(50) NOT NULL, name VARCHAR(100) NOT NULL, precision_scale TINYINT NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active', UNIQUE KEY uk_units_org_code(organization_id,code), FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE materials (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, material_code VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, material_image VARCHAR(500), spec VARCHAR(200), manufacturer_model VARCHAR(200), material_type_id BIGINT, category_id BIGINT NOT NULL, base_unit_id BIGINT, unit VARCHAR(20), status VARCHAR(20) NOT NULL DEFAULT 'active', version INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_materials_org_code(organization_id,material_code), KEY idx_materials_category(category_id), FOREIGN KEY(organization_id) REFERENCES organizations(id) ON DELETE RESTRICT, FOREIGN KEY(material_type_id) REFERENCES material_types(id) ON DELETE RESTRICT, FOREIGN KEY(category_id) REFERENCES material_categories(id) ON DELETE RESTRICT, FOREIGN KEY(base_unit_id) REFERENCES units(id) ON DELETE RESTRICT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE code_rules (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, category_id BIGINT NOT NULL, prefix VARCHAR(30) NOT NULL, sequence_length TINYINT NOT NULL, next_sequence BIGINT NOT NULL DEFAULT 1, status VARCHAR(20) NOT NULL DEFAULT 'active', UNIQUE KEY uk_code_rules_org_category(organization_id,category_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(category_id) REFERENCES material_categories(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_unit_conversions (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, material_id BIGINT NOT NULL, from_unit_id BIGINT NOT NULL, to_unit_id BIGINT NOT NULL, conversion_rate DECIMAL(20,6) NOT NULL, effective_from DATE NOT NULL, effective_to DATE NULL, KEY idx_material_unit_conversions_material(organization_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(material_id) REFERENCES materials(id), FOREIGN KEY(from_unit_id) REFERENCES units(id), FOREIGN KEY(to_unit_id) REFERENCES units(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE tags (id BIGINT AUTO_INCREMENT PRIMARY KEY, material_id BIGINT NOT NULL, tag_code VARCHAR(100) NOT NULL UNIQUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_tags_material(material_id), FOREIGN KEY(material_id) REFERENCES materials(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE business_partners (id BIGINT AUTO_INCREMENT PRIMARY KEY, legal_name VARCHAR(200) NOT NULL, unified_credit_code VARCHAR(50) UNIQUE, contact_name VARCHAR(100), contact_phone VARCHAR(50), address VARCHAR(500), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE organization_partners (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, business_partner_id BIGINT NOT NULL, partner_code VARCHAR(50) NOT NULL, is_supplier BOOLEAN NOT NULL DEFAULT FALSE, is_customer BOOLEAN NOT NULL DEFAULT FALSE, supplier_status VARCHAR(20), customer_status VARCHAR(20), settlement_currency CHAR(3), payment_terms VARCHAR(200), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_organization_partners_org_partner(organization_id,business_partner_id), UNIQUE KEY uk_organization_partners_org_code(organization_id,partner_code), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(business_partner_id) REFERENCES business_partners(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_suppliers (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, material_id BIGINT NOT NULL, organization_partner_id BIGINT NOT NULL, price DECIMAL(20,6) NOT NULL, currency CHAR(3) NOT NULL, minimum_order_quantity DECIMAL(20,6) NOT NULL DEFAULT 0, lead_time_days INT NOT NULL DEFAULT 0, is_preferred BOOLEAN NOT NULL DEFAULT FALSE, effective_from DATE NOT NULL, effective_to DATE, status VARCHAR(20) NOT NULL DEFAULT 'active', KEY idx_material_suppliers_material(organization_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(material_id) REFERENCES materials(id), FOREIGN KEY(organization_partner_id) REFERENCES organization_partners(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE warehouses (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, code VARCHAR(50) NOT NULL, name VARCHAR(150) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_warehouses_org_code(organization_id,code), FOREIGN KEY(organization_id) REFERENCES organizations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE warehouse_locations (id BIGINT AUTO_INCREMENT PRIMARY KEY, warehouse_id BIGINT NOT NULL, code VARCHAR(50) NOT NULL, name VARCHAR(150) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_warehouse_locations_warehouse_code(warehouse_id,code), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_warehouses (user_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(user_id,warehouse_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(warehouse_id) REFERENCES warehouses(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE inventory_balances (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, location_id BIGINT NOT NULL, material_id BIGINT NOT NULL, on_hand DECIMAL(20,6) NOT NULL DEFAULT 0, reserved DECIMAL(20,6) NOT NULL DEFAULT 0, locked DECIMAL(20,6) NOT NULL DEFAULT 0, in_transit DECIMAL(20,6) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 1, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_inventory_balance_dimension(organization_id,warehouse_id,location_id,material_id), KEY idx_inventory_balances_material(organization_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE stock_warning_rules (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, material_id BIGINT NOT NULL, minimum_available DECIMAL(20,6) NOT NULL DEFAULT 0, enabled BOOLEAN NOT NULL DEFAULT TRUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_stock_warning_rules_dimension(organization_id,warehouse_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE inbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, inbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, supplier VARCHAR(200), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_inbound_orders_date(inbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE inbound_items (id BIGINT AUTO_INCREMENT PRIMARY KEY, inbound_order_id BIGINT NOT NULL, material_id BIGINT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_inbound_items_order(inbound_order_id), FOREIGN KEY(inbound_order_id) REFERENCES inbound_orders(id) ON DELETE CASCADE, FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE outbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, outbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, receiver VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_outbound_orders_date(outbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE outbound_items (id BIGINT AUTO_INCREMENT PRIMARY KEY, outbound_order_id BIGINT NOT NULL, material_id BIGINT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_outbound_items_order(outbound_order_id), FOREIGN KEY(outbound_order_id) REFERENCES outbound_orders(id) ON DELETE CASCADE, FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_logs (id BIGINT AUTO_INCREMENT PRIMARY KEY, operation_type VARCHAR(50) NOT NULL, material_id BIGINT NOT NULL, old_value TEXT, new_value TEXT, operator VARCHAR(100), description TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_material_logs_material(material_id), FOREIGN KEY(material_id) REFERENCES materials(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE stock_logs (id BIGINT AUTO_INCREMENT PRIMARY KEY, material_id BIGINT NOT NULL, type ENUM('in','out') NOT NULL, quantity INT NOT NULL, related_order_type VARCHAR(50) NOT NULL, related_order_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_stock_logs_material(material_id), KEY idx_stock_logs_related_order(related_order_type,related_order_id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_applications (id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, purpose VARCHAR(500) NOT NULL DEFAULT '', status ENUM('pending','approved','rejected','cancelled') NOT NULL DEFAULT 'pending', rejection_reason VARCHAR(500) NOT NULL DEFAULT '', approved_by BIGINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE application_items (id BIGINT AUTO_INCREMENT PRIMARY KEY, application_id BIGINT NOT NULL, material_id BIGINT NOT NULL, quantity INT NOT NULL, FOREIGN KEY(application_id) REFERENCES material_applications(id) ON DELETE CASCADE, FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: tags
-- ============================================
CREATE TABLE IF NOT EXISTS tags (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
material_id BIGINT NOT NULL,
tag_code VARCHAR(100) NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_material_id (material_id),
INDEX idx_tag_code (tag_code),
CONSTRAINT fk_tags_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: apply_orders
-- ============================================
CREATE TABLE IF NOT EXISTS apply_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
applicant VARCHAR(100) NOT NULL,
apply_date DATE NOT NULL,
status ENUM('draft', 'pending', 'approved', 'rejected') DEFAULT 'draft',
approval_comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_applicant (applicant),
INDEX idx_status (status),
INDEX idx_apply_date (apply_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: apply_items
-- ============================================
CREATE TABLE IF NOT EXISTS apply_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
apply_order_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
quantity INT NOT NULL,
purpose TEXT,
INDEX idx_apply_order_id (apply_order_id),
INDEX idx_material_id (material_id),
CONSTRAINT fk_apply_items_order FOREIGN KEY (apply_order_id) REFERENCES apply_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_apply_items_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: inbound_orders
-- ============================================
CREATE TABLE IF NOT EXISTS inbound_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
inbound_date DATETIME NOT NULL,
handler VARCHAR(100) NOT NULL,
supplier VARCHAR(200),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_inbound_date (inbound_date),
INDEX idx_handler (handler)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: inbound_items
-- ============================================
CREATE TABLE IF NOT EXISTS inbound_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
inbound_order_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
quantity INT NOT NULL,
INDEX idx_inbound_order_id (inbound_order_id),
INDEX idx_material_id (material_id),
CONSTRAINT fk_inbound_items_order FOREIGN KEY (inbound_order_id) REFERENCES inbound_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_inbound_items_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: outbound_orders
-- ============================================
CREATE TABLE IF NOT EXISTS outbound_orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
outbound_date DATETIME NOT NULL,
handler VARCHAR(100) NOT NULL,
receiver VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_outbound_date (outbound_date),
INDEX idx_handler (handler),
INDEX idx_receiver (receiver)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: outbound_items
-- ============================================
CREATE TABLE IF NOT EXISTS outbound_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
outbound_order_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
quantity INT NOT NULL,
INDEX idx_outbound_order_id (outbound_order_id),
INDEX idx_material_id (material_id),
CONSTRAINT fk_outbound_items_order FOREIGN KEY (outbound_order_id) REFERENCES outbound_orders(id) ON DELETE CASCADE,
CONSTRAINT fk_outbound_items_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: material_logs
-- ============================================
CREATE TABLE IF NOT EXISTS material_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
operation_type VARCHAR(50) NOT NULL,
material_id BIGINT NOT NULL,
old_value TEXT,
new_value TEXT,
operator VARCHAR(100),
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_operation_type (operation_type),
INDEX idx_material_id (material_id),
INDEX idx_created_at (created_at),
CONSTRAINT fk_material_logs_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: stock_logs
-- ============================================
CREATE TABLE IF NOT EXISTS stock_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
material_id BIGINT NOT NULL,
type ENUM('in', 'out') NOT NULL,
quantity INT NOT NULL,
related_order_type VARCHAR(50) NOT NULL,
related_order_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_material_id (material_id),
INDEX idx_type (type),
INDEX idx_related_order (related_order_type, related_order_id),
CONSTRAINT fk_stock_logs_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Table: users
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin','user') NOT NULL DEFAULT 'user',
description VARCHAR(500) DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================
-- Category Code Reference
-- A01-A07: 标准件, 成品件, 简易加工件, 精密加工件, 钣金件, 塑胶件, 其他
-- B01-B07: 成品件, 简易加工件, PCB硬板, FPC软板, 线束, 组装组件, 其他
-- C01-C03: 连接耗材, 密封耗材, 其他
-- D01-D04: 自研程序固件, 外购软件授权, 授权介质, 其他
-- E01-E02: 成品, 半成品
-- F01-F05: 资料文档, 标识标牌, 包装物料, 随机工具配件, 其他
-- ============================================
-- ============================================
-- Table: materials_category (reference table)
-- ============================================
CREATE TABLE IF NOT EXISTS materials_category (
category_code VARCHAR(10) PRIMARY KEY,
category_name VARCHAR(100) NOT NULL,
parent_category VARCHAR(10),
description VARCHAR(200)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Insert category reference data
INSERT INTO materials_category (category_code, category_name, description) VALUES
('A01', '标准件', 'Standard parts'),
('A02', '成品件', 'Finished parts'),
('A03', '简易加工件', 'Simple machined parts'),
('A04', '精密加工件', 'Precision machined parts'),
('A05', '钣金件', 'Sheet metal parts'),
('A06', '塑胶件', 'Plastic parts'),
('A07', '其他', 'Other (A series)'),
('B01', '成品件', 'Finished parts'),
('B02', '简易加工件', 'Simple machined parts'),
('B03', 'PCB硬板', 'PCB hard board'),
('B04', 'FPC软板', 'FPC flexible board'),
('B05', '线束', 'Wire harness'),
('B06', '组装组件', 'Assembly component'),
('B07', '其他', 'Other (B series)'),
('C01', '连接耗材', 'Connection consumable'),
('C02', '密封耗材', 'Sealing consumable'),
('C03', '其他', 'Other (C series)'),
('D01', '自研程序固件', 'In-house firmware'),
('D02', '外购软件授权', 'Purchased software license'),
('D03', '授权介质', 'License media'),
('D04', '其他', 'Other (D series)'),
('E01', '成品', 'Finished product'),
('E02', '半成品', 'Semi-finished product'),
('F01', '资料文档', 'Documentation'),
('F02', '标识标牌', 'Label/signage'),
('F03', '包装物料', 'Packaging material'),
('F04', '随机工具配件', 'Bundled tools/accessories'),
('F05', '其他', 'Other (F series)');
-- ============================================
-- Test Data: 5 Materials (using category codes)
-- ============================================
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-001', 'Steel Plate', '1000x2000x5mm', 'A01', 'Sheet', 50, 200),
('MAT-002', 'Aluminum Rod', 'D20x1000mm', 'A01', 'Piece', 100, 500),
('MAT-003', 'Hex Bolt M10', 'M10x30mm SS304', 'A01', 'Box', 20, 80),
('MAT-004', 'Hydraulic Oil', 'ISO VG 46', 'C01', 'Barrel', 10, 25),
('MAT-005', 'Bearing 6205', '6205-2RS Deep Groove', 'A02', 'Piece', 30, 120);
INSERT INTO organizations (id,code,name,status) VALUES (1,'DEFAULT','默认公司组织','active');
INSERT INTO roles (code,name) VALUES ('super_admin','超级管理员'),('warehouse_admin','仓库管理员'),('purchaser','采购人员'),('technician','技术人员'),('production','生产人员'),('sales','销售人员');
INSERT INTO permissions (code,name) VALUES ('master.material.read','查看物料'),('master.material.write','维护物料'),('master.config.write','维护物料基础配置'),('organization.read','查看公司组织'),('organization.write','维护公司组织'),('organization.user.write','维护用户'),('organization.role.write','维护角色与权限'),('partner.supplier.read','查看供应商'),('partner.supplier.write','维护供应商'),('partner.customer.read','查看客户'),('partner.customer.write','维护客户'),('inventory.stock.read','查看库存'),('inventory.warning.write','维护库存预警'),('inventory.inbound.write','维护入库'),('inventory.outbound.write','维护出库');
INSERT INTO role_permissions (role_id,permission_id) SELECT r.id,p.id FROM roles r CROSS JOIN permissions p WHERE r.code='super_admin';
INSERT INTO material_types (organization_id,code,name) VALUES (1,'raw_material','原材料'),(1,'semi_finished','半成品'),(1,'finished_good','成品'),(1,'consumable','耗材'),(1,'service','服务');
INSERT INTO material_categories (organization_id,code,name) VALUES (1,'A01','标准件'),(1,'A02','成品件'),(1,'A03','简易加工件'),(1,'A04','精密加工件'),(1,'A05','钣金件'),(1,'A06','塑胶件'),(1,'A07','其他'),(1,'B01','外购件类'),(1,'B02','简易加工件'),(1,'B03','PCB硬板'),(1,'B04','FPC软板'),(1,'B05','线束'),(1,'B06','组装组件'),(1,'B07','其他'),(1,'C01','连接耗材'),(1,'C02','密封耗材'),(1,'C03','其他'),(1,'D01','自研程序固件'),(1,'D02','外购软件授权'),(1,'D03','授权介质'),(1,'D04','其他'),(1,'E01','成品'),(1,'E02','半成品'),(1,'F01','资料文档'),(1,'F02','标识标牌'),(1,'F03','包装物料'),(1,'F04','随机工具配件'),(1,'F05','其他');
INSERT INTO material_categories (organization_id,code,name,parent_id,status)
SELECT organization_id,CONCAT(code,'-01'),CONCAT(name,'·通用型'),id,status FROM material_categories WHERE parent_id IS NULL;
INSERT INTO material_categories (organization_id,code,name,parent_id,status)
SELECT organization_id,CONCAT(code,'-02'),CONCAT(name,'·专用型'),id,status FROM material_categories WHERE parent_id IS NULL;
INSERT INTO warehouses (id,organization_id,code,name,status) VALUES (1,1,'DEFAULT','默认仓库','active');
INSERT INTO warehouse_locations (id,warehouse_id,code,name,status) VALUES (1,1,'MAIN','主库位','active');

View File

@ -1,108 +0,0 @@
CREATE TABLE organizations (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
default_timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_organizations_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO organizations (id, code, name, status)
VALUES (1, 'DEFAULT', '默认公司组织', 'active');
CREATE TABLE roles (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(80) NOT NULL,
name VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_roles_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE permissions (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(120) NOT NULL,
name VARCHAR(150) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_permissions_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (role_id, permission_id),
CONSTRAINT fk_role_permissions_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
CONSTRAINT fk_role_permissions_permission FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id),
CONSTRAINT fk_user_roles_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_roles_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_organizations (
user_id BIGINT NOT NULL,
organization_id BIGINT NOT NULL,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, organization_id),
CONSTRAINT fk_user_organizations_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_organizations_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE revoked_tokens (
jti VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (jti),
KEY idx_revoked_tokens_expires_at (expires_at),
CONSTRAINT fk_revoked_tokens_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO roles (code, name) VALUES
('super_admin', '超级管理员'),
('warehouse_admin', '仓库管理员'),
('purchaser', '采购人员'),
('technician', '技术人员'),
('production', '生产人员'),
('sales', '销售人员');
INSERT INTO permissions (code, name) VALUES
('master.material.read', '查看物料'),
('master.material.write', '维护物料'),
('master.config.write', '维护物料基础配置'),
('organization.read', '查看公司组织'),
('organization.write', '维护公司组织'),
('organization.user.write', '维护用户'),
('organization.role.write', '维护角色与权限'),
('partner.supplier.read', '查看供应商'),
('partner.supplier.write', '维护供应商'),
('partner.customer.read', '查看客户'),
('partner.customer.write', '维护客户'),
('inventory.stock.read', '查看库存'),
('inventory.warning.write', '维护库存预警'),
('inventory.inbound.write', '维护入库'),
('inventory.outbound.write', '维护出库');
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r CROSS JOIN permissions p WHERE r.code = 'super_admin';
INSERT INTO user_organizations (user_id, organization_id, is_default)
SELECT id, 1, TRUE FROM users;
INSERT INTO user_roles (user_id, role_id)
SELECT u.id, r.id
FROM users u
JOIN roles r ON r.code = CASE WHEN u.role = 'admin' THEN 'super_admin' ELSE 'warehouse_admin' END;

View File

@ -1,100 +0,0 @@
CREATE TABLE material_types (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
UNIQUE KEY uk_material_types_org_code (organization_id, code),
CONSTRAINT fk_material_types_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_categories (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
parent_id BIGINT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
UNIQUE KEY uk_material_categories_org_code (organization_id, code),
CONSTRAINT fk_material_categories_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_categories_parent FOREIGN KEY (parent_id) REFERENCES material_categories(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE units (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(100) NOT NULL,
precision_scale TINYINT NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
UNIQUE KEY uk_units_org_code (organization_id, code),
CONSTRAINT fk_units_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE code_rules (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
category_id BIGINT NOT NULL,
prefix VARCHAR(30) NOT NULL,
sequence_length TINYINT NOT NULL,
next_sequence BIGINT NOT NULL DEFAULT 1,
status VARCHAR(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
UNIQUE KEY uk_code_rules_org_category (organization_id, category_id),
CONSTRAINT fk_code_rules_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_code_rules_category FOREIGN KEY (category_id) REFERENCES material_categories(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_unit_conversions (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
from_unit_id BIGINT NOT NULL,
to_unit_id BIGINT NOT NULL,
conversion_rate DECIMAL(20,6) NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE NULL,
PRIMARY KEY (id),
KEY idx_material_unit_conversions_material (organization_id, material_id),
CONSTRAINT fk_material_unit_conversions_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_unit_conversions_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_unit_conversions_from_unit FOREIGN KEY (from_unit_id) REFERENCES units(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_unit_conversions_to_unit FOREIGN KEY (to_unit_id) REFERENCES units(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE materials
ADD COLUMN organization_id BIGINT NOT NULL DEFAULT 1 AFTER id,
ADD COLUMN material_type_id BIGINT NULL AFTER category,
ADD COLUMN category_id BIGINT NULL AFTER material_type_id,
ADD COLUMN base_unit_id BIGINT NULL AFTER unit,
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active' AFTER base_unit_id,
ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER status,
ADD COLUMN manufacturer_model VARCHAR(200) NULL AFTER spec,
ADD CONSTRAINT fk_materials_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_materials_type FOREIGN KEY (material_type_id) REFERENCES material_types(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_materials_category FOREIGN KEY (category_id) REFERENCES material_categories(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_materials_base_unit FOREIGN KEY (base_unit_id) REFERENCES units(id) ON DELETE RESTRICT;
ALTER TABLE materials DROP INDEX material_code;
ALTER TABLE materials ADD UNIQUE KEY uk_materials_org_code (organization_id, material_code);
INSERT INTO material_types (organization_id, code, name) VALUES
(1, 'raw_material', '原材料'),
(1, 'semi_finished', '半成品'),
(1, 'finished_good', '成品'),
(1, 'consumable', '耗材'),
(1, 'service', '服务');
INSERT INTO material_categories (organization_id, code, name)
SELECT 1, category_code, category_name FROM materials_category;
INSERT INTO units (organization_id, code, name)
SELECT 1, unit, unit FROM materials WHERE unit IS NOT NULL AND unit <> '' GROUP BY unit;
UPDATE materials m
LEFT JOIN material_categories c ON c.organization_id = m.organization_id AND c.code = m.category
LEFT JOIN units u ON u.organization_id = m.organization_id AND u.code = m.unit
SET m.category_id = c.id, m.base_unit_id = u.id;

View File

@ -1,52 +0,0 @@
CREATE TABLE business_partners (
id BIGINT NOT NULL AUTO_INCREMENT,
legal_name VARCHAR(200) NOT NULL,
unified_credit_code VARCHAR(50) NULL,
contact_name VARCHAR(100) NULL,
contact_phone VARCHAR(50) NULL,
address VARCHAR(500) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_business_partners_credit_code (unified_credit_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE organization_partners (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
business_partner_id BIGINT NOT NULL,
partner_code VARCHAR(50) NOT NULL,
is_supplier BOOLEAN NOT NULL DEFAULT FALSE,
is_customer BOOLEAN NOT NULL DEFAULT FALSE,
supplier_status VARCHAR(20) NULL,
customer_status VARCHAR(20) NULL,
settlement_currency CHAR(3) NULL,
payment_terms VARCHAR(200) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_organization_partners_org_partner (organization_id, business_partner_id),
UNIQUE KEY uk_organization_partners_org_code (organization_id, partner_code),
CONSTRAINT fk_organization_partners_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_organization_partners_partner FOREIGN KEY (business_partner_id) REFERENCES business_partners(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE material_suppliers (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
organization_partner_id BIGINT NOT NULL,
price DECIMAL(20,6) NOT NULL,
currency CHAR(3) NOT NULL,
minimum_order_quantity DECIMAL(20,6) NOT NULL DEFAULT 0,
lead_time_days INT NOT NULL DEFAULT 0,
is_preferred BOOLEAN NOT NULL DEFAULT FALSE,
effective_from DATE NOT NULL,
effective_to DATE NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
PRIMARY KEY (id),
KEY idx_material_suppliers_material (organization_id, material_id),
CONSTRAINT fk_material_suppliers_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_suppliers_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT,
CONSTRAINT fk_material_suppliers_partner FOREIGN KEY (organization_partner_id) REFERENCES organization_partners(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,43 +0,0 @@
CREATE TABLE warehouses (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(150) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_warehouses_org_code (organization_id, code),
CONSTRAINT fk_warehouses_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE warehouse_locations (
id BIGINT NOT NULL AUTO_INCREMENT,
warehouse_id BIGINT NOT NULL,
code VARCHAR(50) NOT NULL,
name VARCHAR(150) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_warehouse_locations_warehouse_code (warehouse_id, code),
CONSTRAINT fk_warehouse_locations_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE user_warehouses (
user_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, warehouse_id),
CONSTRAINT fk_user_warehouses_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_user_warehouses_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO warehouses (id, organization_id, code, name, status)
VALUES (1, 1, 'DEFAULT', '默认仓库', 'active');
INSERT INTO warehouse_locations (id, warehouse_id, code, name, status)
VALUES (1, 1, 'MAIN', '主库位', 'active');
INSERT INTO user_warehouses (user_id, warehouse_id)
SELECT id, 1 FROM users;

View File

@ -1,36 +0,0 @@
CREATE TABLE inventory_balances (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
location_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
on_hand DECIMAL(20,6) NOT NULL DEFAULT 0,
reserved DECIMAL(20,6) NOT NULL DEFAULT 0,
locked DECIMAL(20,6) NOT NULL DEFAULT 0,
in_transit DECIMAL(20,6) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_inventory_balance_dimension (organization_id, warehouse_id, location_id, material_id),
KEY idx_inventory_balances_material (organization_id, material_id),
CONSTRAINT fk_inventory_balances_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_inventory_balances_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT,
CONSTRAINT fk_inventory_balances_location FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE RESTRICT,
CONSTRAINT fk_inventory_balances_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE stock_warning_rules (
id BIGINT NOT NULL AUTO_INCREMENT,
organization_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
material_id BIGINT NOT NULL,
minimum_available DECIMAL(20,6) NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_stock_warning_rules_dimension (organization_id, warehouse_id, material_id),
CONSTRAINT fk_stock_warning_rules_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
CONSTRAINT fk_stock_warning_rules_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT,
CONSTRAINT fk_stock_warning_rules_material FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,31 +0,0 @@
INSERT INTO inventory_balances (organization_id, warehouse_id, location_id, material_id, on_hand)
SELECT 1, 1, 1, id, current_stock FROM materials
ON DUPLICATE KEY UPDATE material_id = VALUES(material_id);
INSERT INTO stock_warning_rules (organization_id, warehouse_id, material_id, minimum_available, enabled)
SELECT 1, 1, id, warning_stock, TRUE FROM materials
ON DUPLICATE KEY UPDATE material_id = VALUES(material_id);
ALTER TABLE inbound_orders
ADD COLUMN organization_id BIGINT NOT NULL DEFAULT 1 AFTER id,
ADD COLUMN warehouse_id BIGINT NOT NULL DEFAULT 1 AFTER organization_id,
ADD COLUMN location_id BIGINT NOT NULL DEFAULT 1 AFTER warehouse_id,
ADD CONSTRAINT fk_inbound_orders_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_inbound_orders_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_inbound_orders_location FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE RESTRICT;
ALTER TABLE outbound_orders
ADD COLUMN organization_id BIGINT NOT NULL DEFAULT 1 AFTER id,
ADD COLUMN warehouse_id BIGINT NOT NULL DEFAULT 1 AFTER organization_id,
ADD COLUMN location_id BIGINT NOT NULL DEFAULT 1 AFTER warehouse_id,
ADD CONSTRAINT fk_outbound_orders_org FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_outbound_orders_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_outbound_orders_location FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE RESTRICT;
SELECT m.id, m.material_code, m.current_stock AS legacy_stock,
COALESCE(SUM(ib.on_hand), 0) AS migrated_stock,
m.current_stock - COALESCE(SUM(ib.on_hand), 0) AS difference
FROM materials m
LEFT JOIN inventory_balances ib ON ib.material_id = m.id
GROUP BY m.id, m.material_code, m.current_stock
HAVING difference <> 0;

View File

@ -1,3 +0,0 @@
ALTER TABLE materials
DROP COLUMN current_stock,
DROP COLUMN warning_stock;

View File

@ -1,31 +0,0 @@
INSERT IGNORE INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM (
SELECT 'warehouse_admin' AS role_code, 'master.material.read' AS permission_code
UNION ALL SELECT 'warehouse_admin', 'inventory.stock.read'
UNION ALL SELECT 'warehouse_admin', 'inventory.warning.write'
UNION ALL SELECT 'warehouse_admin', 'inventory.inbound.write'
UNION ALL SELECT 'warehouse_admin', 'inventory.outbound.write'
UNION ALL SELECT 'purchaser', 'master.material.read'
UNION ALL SELECT 'purchaser', 'partner.supplier.read'
UNION ALL SELECT 'purchaser', 'partner.supplier.write'
UNION ALL SELECT 'purchaser', 'inventory.stock.read'
UNION ALL SELECT 'purchaser', 'inventory.warning.write'
UNION ALL SELECT 'technician', 'master.material.read'
UNION ALL SELECT 'technician', 'master.material.write'
UNION ALL SELECT 'technician', 'master.config.write'
UNION ALL SELECT 'technician', 'inventory.stock.read'
UNION ALL SELECT 'production', 'master.material.read'
UNION ALL SELECT 'production', 'inventory.stock.read'
UNION ALL SELECT 'production', 'inventory.outbound.write'
UNION ALL SELECT 'sales', 'master.material.read'
UNION ALL SELECT 'sales', 'partner.customer.read'
UNION ALL SELECT 'sales', 'partner.customer.write'
UNION ALL SELECT 'sales', 'inventory.stock.read'
) AS defaults
JOIN roles r ON r.code = defaults.role_code
JOIN permissions p ON p.code = defaults.permission_code;

View File

@ -1 +0,0 @@
ALTER TABLE users DROP COLUMN role;

View File

@ -1,96 +0,0 @@
UPDATE material_categories
SET name = '外购件类'
WHERE code = 'B01' AND parent_id IS NULL;
INSERT INTO material_categories (organization_id, code, name, parent_id, status)
SELECT parent.organization_id,
CONCAT(parent.code, '-01'),
CASE parent.code
WHEN 'A01' THEN '紧固件类'
WHEN 'A02' THEN '轴承类'
WHEN 'A03' THEN '轴套法兰类'
WHEN 'A04' THEN '导轨丝杠类'
WHEN 'A05' THEN '机柜钣金类'
WHEN 'A06' THEN '管槽塑胶类'
WHEN 'B01' THEN '电子类'
WHEN 'B03' THEN '控制板类'
WHEN 'B04' THEN '排线类'
WHEN 'B05' THEN '电源线束类'
WHEN 'C01' THEN '接线辅材类'
WHEN 'C02' THEN '密封件类'
WHEN 'D01' THEN '控制固件类'
WHEN 'D02' THEN '工业软件类'
WHEN 'E01' THEN '电气设备类'
WHEN 'F01' THEN '技术资料类'
WHEN 'F02' THEN '设备标识类'
WHEN 'F03' THEN '包装容器类'
WHEN 'F04' THEN '手动工具类'
ELSE CONCAT(parent.name, '·通用型')
END,
parent.id,
parent.status
FROM material_categories parent
WHERE parent.parent_id IS NULL
AND NOT EXISTS (
SELECT 1 FROM material_categories child
WHERE child.organization_id = parent.organization_id
AND child.parent_id = parent.id
);
INSERT INTO material_categories (organization_id, code, name, parent_id, status)
SELECT parent.organization_id,
CONCAT(parent.code, '-02'),
CASE parent.code
WHEN 'A01' THEN '传动连接类'
WHEN 'A02' THEN '其他成品件类'
WHEN 'A03' THEN '连接加工件类'
WHEN 'A04' THEN '齿轮传动类'
WHEN 'A05' THEN '管槽接头类'
WHEN 'A06' THEN '密封防护类'
WHEN 'B01' THEN '机电类'
WHEN 'B03' THEN '功能板类'
WHEN 'B04' THEN '柔性电缆类'
WHEN 'B05' THEN '信号线束类'
WHEN 'C01' THEN '包装连接类'
WHEN 'C02' THEN '密封材料类'
WHEN 'D01' THEN '升级包类'
WHEN 'D02' THEN '设计软件类'
WHEN 'E01' THEN '操作设备类'
WHEN 'F01' THEN '管理资料类'
WHEN 'F02' THEN '包装标识类'
WHEN 'F03' THEN '缓冲防护类'
WHEN 'F04' THEN '仪器仪表类'
ELSE CONCAT(parent.name, '·专用型')
END,
parent.id,
parent.status
FROM material_categories parent
WHERE parent.parent_id IS NULL
AND EXISTS (
SELECT 1 FROM material_categories first_child
WHERE first_child.organization_id = parent.organization_id
AND first_child.parent_id = parent.id
AND first_child.code = CONCAT(parent.code, '-01')
)
AND NOT EXISTS (
SELECT 1 FROM material_categories child
WHERE child.organization_id = parent.organization_id
AND child.code = CONCAT(parent.code, '-02')
);
UPDATE materials material
JOIN material_categories parent
ON parent.id = material.category_id
AND parent.organization_id = material.organization_id
AND parent.parent_id IS NULL
JOIN material_categories child
ON child.id = (
SELECT CASE
WHEN MOD(CRC32(material.material_code), 2) = 0 THEN MIN(candidate.id)
ELSE MAX(candidate.id)
END
FROM material_categories candidate
WHERE candidate.organization_id = parent.organization_id
AND candidate.parent_id = parent.id
)
SET material.category_id = child.id;

View File

@ -1,2 +0,0 @@
ALTER TABLE materials
MODIFY COLUMN category_id BIGINT NOT NULL;

View File

@ -1,138 +0,0 @@
-- ============================================
-- Test Data for WMMS
-- 遵循命名规范:物料代码 MAT-XXX
-- 分类使用 materials_category 表中的标准分类码
-- ============================================
USE wmms;
-- A01 类:标准件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-006', '六角螺栓 M12', 'M12x40mm 镀锌', 'A01', '盒', 50, 300),
('MAT-007', '平垫圈 M12', 'M12 镀锌平垫', 'A01', '盒', 100, 500),
('MAT-008', '弹垫 M12', 'M12 弹簧垫圈 65Mn', 'A01', '盒', 80, 400),
('MAT-009', '内六角螺丝 M8', 'M8x25mm 不锈钢304', 'A01', '盒', 60, 250),
('MAT-010', '双头螺栓', 'M16x100mm 4.8级', 'A01', '根', 40, 180);
-- A02 类:成品件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-011', '深沟球轴承 6204', '6204-2RS 橡胶密封', 'A02', '个', 30, 120),
('MAT-012', '深沟球轴承 6206', '6206-2RS 橡胶密封', 'A02', '个', 25, 100),
('MAT-013', '角接触轴承 7205', '7205AC 接触角25°', 'A02', '个', 20, 80),
('MAT-014', '圆锥滚子轴承 30205', '30205 新疆特检院', 'A02', '个', 15, 60),
('MAT-015', '直线轴承 LM8UU', 'LM8UU 直线运动', 'A02', '个', 40, 160);
-- A03 类:简易加工件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-016', '轴套 45#钢', 'Φ25x30mm 调质处理', 'A03', '个', 50, 200),
('MAT-017', '法兰盘', 'Φ80x20mm 45#钢', 'A03', '个', 30, 120),
('MAT-018', '锁紧螺母', 'M30x1.5 尼龙锁紧', 'A03', '个', 40, 180),
('MAT-019', '调整垫片', 'Φ50/Φ25x0.5mm 45#钢', 'A03', '片', 100, 500),
('MAT-020', '连接套筒', 'Φ40x60mm 45#钢', 'A03', '个', 25, 100);
-- A04 类:精密加工件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-021', '精密丝杠', 'T12x4mm 长度500mm 研磨级', 'A04', '根', 5, 20),
('MAT-022', '导轨滑块', 'MGN12H 长度100mm', 'A04', '个', 10, 40),
('MAT-023', '分度盘', 'Φ100mm 72等分 45#钢', 'A04', '个', 3, 15),
('MAT-024', '齿轮坯料', 'M2 Z=40 45#钢 调质', 'A04', '个', 20, 80),
('MAT-025', '蜗轮坯料', '模数3 齿数50 青铜', 'A04', '个', 8, 35);
-- A05 类:钣金件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-026', '安装板', '300x200x5mm Q235A', 'A05', '块', 15, 60),
('MAT-027', '电气柜门板', '600x400x1.5mm 喷塑', 'A05', '块', 8, 30),
('MAT-028', '机箱外壳', '400x300x150mm 铝合金', 'A05', '个', 10, 40),
('MAT-029', '波纹管接头', 'DG25 不锈钢', 'A05', '个', 50, 200),
('MAT-030', '线槽', '50x50x2000mm 铝合金', 'A05', '根', 20, 80);
-- A06 类:塑胶件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-031', 'PVC管 25', 'Φ25x3mm 长度2m', 'A06', '根', 30, 120),
('MAT-032', '塑料线槽', '20x10mm PVC白色', 'A06', '根', 50, 200),
('MAT-033', '尼龙轮', 'Φ50mm 带轴承', 'A06', '个', 20, 80),
('MAT-034', '橡胶密封圈', 'Φ30x3 NBR70', 'A06', '个', 100, 400),
('MAT-035', 'PE保护盖', 'M20x1.5 塑料', 'A06', '个', 200, 800);
-- B01 类:成品件(电子)
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-036', 'PLC控制器 S7-200', 'CPU SR40 DC/DC/DC', 'B01', '台', 3, 12),
('MAT-037', '变频器', '0.75kW 380V 三相', 'B01', '台', 5, 20),
('MAT-038', '伺服驱动器', '400W 220V', 'B01', '台', 4, 15),
('MAT-039', '触摸屏', '7寸 800x480 RS485', 'B01', '台', 3, 10),
('MAT-040', '开关电源', '24V 10A 导轨式', 'B01', '个', 10, 40);
-- B03 类:PCB硬板
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-041', '控制板 PCB', '150x100mm 双面板', 'B03', '块', 10, 40),
('MAT-042', '驱动板 PCB', '100x80mm 四层板', 'B03', '块', 8, 30),
('MAT-043', '显示板 PCB', '80x50mm 单面板', 'B03', '块', 15, 60),
('MAT-044', '电源板 PCB', '120x90mm 双面板', 'B03', '块', 10, 35);
-- B04 类:FPC软板
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-045', 'FPC排线', '0.5mm间距 50pin', 'B04', '条', 20, 80),
('MAT-046', 'FPC连接线', '1.0mm间距 30pin', 'B04', '条', 15, 60),
('MAT-047', '柔性电缆', '0.3mm间距 20pin', 'B04', '条', 25, 100);
-- B05 类:线束
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-048', '电源线束', '3x2.5平方 RVV', 'B05', '条', 20, 80),
('MAT-049', '信号线束', '4x0.75平方 屏蔽', 'B05', '条', 30, 120),
('MAT-050', '电机线束', '4x4平方 耐油', 'B05', '条', 15, 60),
('MAT-051', '编码器线', '8芯 屏蔽双绞', 'B05', '条', 25, 100),
('MAT-052', '网线', 'CAT6 蓝色 2m', 'B05', '条', 50, 200);
-- C01 类:连接耗材
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-053', '扎带', '3x150mm 尼龙', 'C01', '包', 20, 100),
('MAT-054', '热缩管', 'Φ10 黑色 2m/根', 'C01', '根', 30, 150),
('MAT-055', '接线端子', 'UK 2.5N 弹簧式', 'C01', '个', 200, 800),
('MAT-056', '冷压端头', 'RV2-3.5 叉形', 'C01', '个', 300, 1200),
('MAT-057', '线号管', 'Φ2.5 白色', 'C01', '米', 100, 400),
('MAT-058', '缠绕膜', '500mmx0.025mm', 'C01', '卷', 10, 40);
-- C02 类:密封耗材
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-059', 'O型圈', 'Φ25x3 NBR70', 'C02', '个', 150, 600),
('MAT-060', '骨架油封', 'Φ35x50x10 TC双唇', 'C02', '个', 50, 200),
('MAT-061', '密封胶', '硅酮密封胶 300ml', 'C02', '支', 20, 80),
('MAT-062', '生料带', '18mmx0.1mmx20m', 'C02', '卷', 30, 120),
('MAT-063', '盘根', '10x10mm 柔性石墨', 'C02', '米', 20, 80);
-- D01 类:自研程序固件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-064', '主控程序', 'V2.1.0 HEX文件', 'D01', '套', 1, 5),
('MAT-065', '显示程序', 'V1.8.2 HEX文件', 'D01', '套', 1, 5),
('MAT-066', '固件升级包', 'V3.0.0 BIN文件', 'D01', '套', 2, 8);
-- D02 类:外购软件授权
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-067', '组态软件授权', '昆仑通态 授权key', 'D02', '套', 2, 10),
('MAT-068', 'PLC编程软件', '西门子 V4.0授权', 'D02', '套', 1, 5),
('MAT-069', 'CAD软件授权', '年费授权', 'D02', '套', 3, 10);
-- E01 类:成品
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-070', '控制柜', '800x600x2000mm', 'E01', '台', 2, 8),
('MAT-071', '操作台', '1500x800x1200mm', 'E01', '台', 1, 5),
('MAT-072', '配电箱', '400x300x150mm', 'E01', '台', 3, 12);
-- F03 类:包装物料
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-073', '纸箱', '400x300x300mm', 'F03', '个', 50, 200),
('MAT-074', '气泡膜', '宽1m 厚3mm', 'F03', '卷', 10, 40),
('MAT-075', '木托盘', '1200x1000x150mm', 'F03', '个', 20, 80),
('MAT-076', '打包带', '12mmx0.8mm', 'F03', '卷', 15, 60),
('MAT-077', '标签纸', '100x60mm 白色', 'F03', '张', 500, 2000);
-- F04 类:随机工具配件
INSERT INTO materials (material_code, name, spec, category, unit, warning_stock, current_stock) VALUES
('MAT-078', '内六角扳手套装', '9件套 公制', 'F04', '套', 5, 20),
('MAT-079', '螺丝刀套装', '6支 公制+十字', 'F04', '套', 8, 30),
('MAT-080', '万用表', '数字式 VC890C+', 'F04', '个', 3, 12),
('MAT-081', '剥线钳', '自动调节 0.5-6mm²', 'F04', '把', 5, 20),
('MAT-082', '压线钳', '电子线专用 0.5-2mm²', 'F04', '把', 5, 20),
('MAT-083', '活动扳手', '10寸', 'F04', '把', 6, 25),
('MAT-084', '呆扳手套装', '8件套 8-24mm', 'F04', '套', 4, 15),
('MAT-085', 'aiman 工具箱', '18寸 塑料', 'F04', '个', 3, 10);