346 lines
13 KiB
PL/PgSQL
346 lines
13 KiB
PL/PgSQL
/* ---------- 基础设置 ---------- */
|
||
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)'; |