csdesign-smart-library/sql/create_library.sql
2025-06-24 10:56:21 +08:00

346 lines
13 KiB
PL/PgSQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ---------- 基础设置 ---------- */
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';