/* ---------- 基础设置 ---------- */ CREATE SCHEMA IF NOT EXISTS library; SET search_path TO library,public; /* ---------- ENUM 类型 ---------- */ CREATE TYPE gender_enum AS ENUM ('M','F','O'); CREATE TYPE book_status_enum AS ENUM ('normal','lost','damaged','removed'); CREATE TYPE borrow_status_enum AS ENUM ('borrowed','returned','overdue','lost'); CREATE TYPE reserve_status_enum AS ENUM ('waiting','available','cancelled','expired'); CREATE TYPE acct_status_enum AS ENUM ('active','reported','frozen'); /* ---------- 系统参数表 ---------- */ CREATE TABLE system_settings ( setting_key text PRIMARY KEY, setting_value text NOT NULL ); /* 罚款费率、冻结阈值等默认值 */ INSERT INTO system_settings(setting_key,setting_value) VALUES ('fine_per_day', '0.50'), -- 每本书每日罚款金额 ('freeze_threshold', '20.00'), -- 欠款 ≥ 此值自动冻结 ('max_borrow_default', '10'); -- 默认最大借阅量 /* ---------- 图书信息 ---------- */ CREATE TABLE books ( book_id bigserial PRIMARY KEY, isbn varchar(13) UNIQUE NOT NULL, title text NOT NULL, authors text[] NOT NULL, publisher text, publish_date date, price numeric(10,2), classification_no varchar(64), location varchar(255), total_copies int NOT NULL DEFAULT 1 CHECK(total_copies>0), available_copies int NOT NULL DEFAULT 1 CHECK(available_copies>=0), status book_status_enum NOT NULL DEFAULT 'normal', description text, cover_url text, created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now() ); /* ---------- 帮助函数:按键名返回 int 型系统参数 ---------- */ CREATE OR REPLACE FUNCTION library.get_setting_int(p_key text) RETURNS int LANGUAGE sql STABLE -- ← 允许做 DEFAULT AS $$ SELECT setting_value::int FROM library.system_settings WHERE setting_key = p_key; $$; /* ---------- 学生信息 ---------- */ CREATE TABLE library.students ( student_id bigserial PRIMARY KEY, stu_no varchar(20) UNIQUE NOT NULL, -- 学号 name varchar(64) NOT NULL, gender gender_enum, department varchar(128), major varchar(128), grade varchar(10), class varchar(50), phone varchar(20), email varchar(255), account_status acct_status_enum NOT NULL DEFAULT 'active', max_borrow int NOT NULL DEFAULT library.get_setting_int('max_borrow_default'), current_borrow int NOT NULL DEFAULT 0, created_at timestamptz DEFAULT now() ); /* ---------- 管理员信息 ---------- */ CREATE TABLE admins ( admin_id bigserial PRIMARY KEY, emp_no varchar(20) UNIQUE NOT NULL, name varchar(64) NOT NULL, position varchar(64), phone varchar(20), privilege_lv int NOT NULL DEFAULT 1, created_at timestamptz DEFAULT now() ); /* ---------- 借阅记录 ---------- */ CREATE TABLE borrow_records ( borrow_id bigserial PRIMARY KEY, book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, borrow_date date NOT NULL DEFAULT current_date, due_date date NOT NULL, return_date date, renew_times int NOT NULL DEFAULT 0, status borrow_status_enum NOT NULL DEFAULT 'borrowed', fine_amount numeric(10,2) NOT NULL DEFAULT 0 ); /* ---------- 图书预约 ---------- */ CREATE TABLE reservations ( reservation_id bigserial PRIMARY KEY, book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, reserve_date date NOT NULL DEFAULT current_date, status reserve_status_enum NOT NULL DEFAULT 'waiting' ); /* ---------- 图书评价 ---------- */ CREATE TABLE reviews ( review_id bigserial PRIMARY KEY, book_id bigint REFERENCES books(book_id) ON DELETE CASCADE, student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, rating int NOT NULL CHECK(rating BETWEEN 1 AND 5), content text, review_time timestamptz DEFAULT now(), UNIQUE(book_id,student_id) -- 一人一书仅一次评价 ); /* ---------- 罚款记录 ---------- */ CREATE TABLE fines ( fine_id bigserial PRIMARY KEY, student_id bigint REFERENCES students(student_id) ON DELETE CASCADE, amount numeric(10,2) NOT NULL CHECK(amount>0), reason text, status varchar(10) NOT NULL CHECK(status IN ('unpaid','paid')), issue_date date NOT NULL DEFAULT current_date, admin_id bigint REFERENCES admins(admin_id) ); /* ---------- 拓展 ---------- */ CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; /* ---------- 索引 ---------- */ CREATE INDEX idx_books_title_trgm ON library.books USING gin (title gin_trgm_ops); CREATE INDEX idx_books_authors_gin ON library.books USING gin (authors); CREATE INDEX idx_br_student_status ON borrow_records(student_id,status); CREATE INDEX idx_reserve_book_wait ON reservations(status,book_id); CREATE INDEX idx_fines_student_unpaid ON fines(student_id) WHERE status='unpaid'; /* 要使用 trigram GIN 索引需启用扩展 */ CREATE EXTENSION IF NOT EXISTS pg_trgm; /* ---------- 触发器函数 ---------- */ /* 1. 更新可借数量 & 学生当前借阅量 */ CREATE OR REPLACE FUNCTION trg_sync_book_student() RETURNS TRIGGER AS $$ BEGIN -- 更新图书可借册数 UPDATE books SET available_copies = total_copies - (SELECT count(*) FROM borrow_records WHERE book_id = COALESCE(NEW.book_id,OLD.book_id) AND status IN ('borrowed','overdue')) WHERE book_id = COALESCE(NEW.book_id,OLD.book_id); -- 更新学生当前借阅量 UPDATE students SET current_borrow = (SELECT count(*) FROM borrow_records WHERE student_id = COALESCE(NEW.student_id,OLD.student_id) AND status IN ('borrowed','overdue')) WHERE student_id = COALESCE(NEW.student_id,OLD.student_id); RETURN NULL; END; $$ LANGUAGE plpgsql; /* 2. 归还时自动计算并记账罚款 */ CREATE OR REPLACE FUNCTION trg_calc_fine() RETURNS TRIGGER AS $$ DECLARE days_overdue int; rate numeric(10,2) := (SELECT setting_value::numeric FROM system_settings WHERE setting_key='fine_per_day'); BEGIN IF TG_OP='UPDATE' AND NEW.status='returned' AND OLD.status IN ('borrowed','overdue') THEN days_overdue := GREATEST((NEW.return_date - NEW.due_date),0); IF days_overdue > 0 THEN INSERT INTO fines(student_id,amount,reason,status,admin_id) VALUES (NEW.student_id, days_overdue*rate, format('Overdue %s days for borrow_id=%s',days_overdue,NEW.borrow_id), 'unpaid', NULL); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; /* 3. 欠款超阈值自动冻结账户 */ CREATE OR REPLACE FUNCTION library.trg_freeze_account() RETURNS TRIGGER AS $$ DECLARE total_unpaid numeric(10,2); threshold numeric(10,2); v_relevant_student_id bigint; v_current_status library.acct_status_enum; v_new_status library.acct_status_enum; BEGIN -- 1. 获取最新的冻结阈值 SELECT setting_value::numeric INTO threshold FROM library.system_settings WHERE setting_key = 'freeze_threshold'; IF NOT FOUND THEN RAISE WARNING 'System setting "freeze_threshold" not found. Account status will not be updated by trigger.'; RETURN NULL; -- 或者 RAISE EXCEPTION; END IF; -- 2. 确定相关的学生ID IF TG_OP = 'DELETE' THEN v_relevant_student_id := OLD.student_id; ELSE -- INSERT or UPDATE v_relevant_student_id := NEW.student_id; END IF; -- 如果没有相关的学生ID(理论上不应发生,因为student_id是FK且NOT NULL),则退出 IF v_relevant_student_id IS NULL THEN RAISE WARNING 'trg_freeze_account: No relevant student_id found. TG_OP: %', TG_OP; RETURN NULL; END IF; -- 3. 计算该学生未缴罚款总额 SELECT COALESCE(sum(amount),0) INTO total_unpaid FROM library.fines WHERE student_id = v_relevant_student_id AND status = 'unpaid'; -- 4. 获取学生当前账户状态 SELECT account_status INTO v_current_status FROM library.students WHERE student_id = v_relevant_student_id; -- 5. 根据罚款确定新的目标状态 -- 注意:原始逻辑是,如果欠款低于阈值,则账户变为 'active'。 -- 这意味着如果账户之前是 'reported'(挂失),且欠款低于阈值,它也会被此触发器改为 'active'。 -- 如果希望 'reported' 状态不受此罚款逻辑影响(除非因欠款被冻结),则需要更复杂的判断。 -- 此处保持与你原触发器相似的逻辑,仅修复类型问题并优化。 IF total_unpaid >= threshold THEN v_new_status := 'frozen'::library.acct_status_enum; ELSE -- 如果当前已经是 'frozen',则解冻为 'active' -- 如果当前不是 'frozen' (比如是 'active' 或 'reported'),且欠款未超限,则应保持其原状态, -- 而不是都强制变为 'active'。 -- 为了更安全地处理 'reported' 状态,我们修改这里的逻辑: IF v_current_status = 'frozen'::library.acct_status_enum THEN v_new_status := 'active'::library.acct_status_enum; ELSE v_new_status := v_current_status; -- 保持现有状态 (active 或 reported) END IF; END IF; -- 6. 如果计算出的新状态与当前状态不同,则更新学生账户状态 IF v_current_status IS DISTINCT FROM v_new_status THEN UPDATE library.students SET account_status = v_new_status WHERE student_id = v_relevant_student_id; END IF; RETURN NULL; -- AFTER 触发器通常返回 NULL END; $$ LANGUAGE plpgsql; /* ---------- 触发器绑定 ---------- */ CREATE TRIGGER trg_borrow_sync_aiud AFTER INSERT OR UPDATE OR DELETE ON borrow_records FOR EACH ROW EXECUTE FUNCTION trg_sync_book_student(); CREATE TRIGGER trg_borrow_calc_fine_upd AFTER UPDATE ON borrow_records FOR EACH ROW EXECUTE FUNCTION trg_calc_fine(); CREATE TRIGGER trg_fine_freeze_aiud AFTER INSERT OR UPDATE OR DELETE ON fines FOR EACH ROW EXECUTE FUNCTION trg_freeze_account(); /* ---------- 视图 ---------- */ /* (1) 当前热门图书:借阅量前 20 */ CREATE OR REPLACE VIEW v_hot_books AS SELECT b.book_id, b.isbn, b.title, COUNT(br.borrow_id) AS borrow_cnt FROM books b JOIN borrow_records br ON br.book_id = b.book_id GROUP BY b.book_id ORDER BY borrow_cnt DESC LIMIT 20; /* (2) 各院系借阅统计 */ CREATE OR REPLACE VIEW v_dept_borrow_stats AS SELECT s.department, COUNT(br.borrow_id) AS total_borrows, COUNT(DISTINCT br.student_id) AS unique_readers FROM borrow_records br JOIN students s ON s.student_id = br.student_id GROUP BY s.department ORDER BY total_borrows DESC; /* (3) 图书逾期情况 */ CREATE OR REPLACE VIEW v_overdue_details AS SELECT br.borrow_id, br.book_id, br.student_id, br.due_date, CURRENT_DATE - br.due_date AS days_overdue FROM borrow_records br WHERE br.status='overdue'; /* ---------- 存储过程(plpgsql 函数) ---------- */ /* 1. 学期初批量初始化学生账户 - 示例:EXECUTE library.sp_init_students('2025-Fall'); */ CREATE OR REPLACE FUNCTION sp_init_students(term_code text) RETURNS void LANGUAGE plpgsql AS $$ BEGIN -- 示例逻辑:所有学生 current_borrow 清零、状态激活 UPDATE students SET current_borrow=0, account_status='active'; RAISE NOTICE 'Students initialized for %', term_code; END; $$; /* 2. 定期生成图书流通统计(存入自定义表) */ CREATE TABLE IF NOT EXISTS circulation_stats ( stat_date date PRIMARY KEY, total_borrows bigint, unique_readers bigint, created_at timestamptz DEFAULT now() ); CREATE OR REPLACE FUNCTION sp_generate_circulation_stats() RETURNS void LANGUAGE plpgsql AS $$ BEGIN INSERT INTO circulation_stats(stat_date,total_borrows,unique_readers) SELECT CURRENT_DATE, (SELECT COUNT(*) FROM borrow_records WHERE borrow_date=CURRENT_DATE), (SELECT COUNT(DISTINCT student_id) FROM borrow_records WHERE borrow_date=CURRENT_DATE); END; $$; /* 3. 自动发送逾期提醒(示例写入提醒表) */ CREATE TABLE IF NOT EXISTS overdue_notices ( notice_id bigserial PRIMARY KEY, borrow_id bigint, student_id bigint, notice_time timestamptz DEFAULT now() ); CREATE OR REPLACE FUNCTION sp_send_overdue_notices() RETURNS void LANGUAGE plpgsql AS $$ BEGIN INSERT INTO overdue_notices(borrow_id,student_id) SELECT br.borrow_id, br.student_id FROM borrow_records br WHERE br.status='overdue' AND NOT EXISTS (SELECT 1 FROM overdue_notices onot WHERE onot.borrow_id=br.borrow_id); END; $$; /* ---------- 完成 ---------- */ COMMENT ON SCHEMA library IS '智能图书管理系统数据库架构(2025-06-21)';