• [技术干货] PostgreSQL 修改默认端口
    在 PostgreSQL 中,默认的端口号是 5432。这意味着,当你安装 PostgreSQL 数据库服务时,它会尝试在这个端口上监听连接请求。如果你需要更改默认端口号,你可以在 PostgreSQL 的配置文件 postgresql.conf 中找到并修改 port 参数的值。例如,如果你想将端口号更改为 5433,你可以这样设置:port = 5433 更改配置后,需要重启 PostgreSQL 服务来使更改生效。这可以通过以下命令完成(取决于你的操作系统和安装方式):对于大多数 Linux 系统:sudo systemctl restart postgresql对于 macOS(使用 Homebrew 安装):brew services restart postgresql对于 Windows:打开“服务”管理工具。找到 PostgreSQL 服务。右键点击并选择“重启”。注意:更改端口号后,如果你使用 psql 命令行工具或其他客户端连接到数据库,你需要指定新的端口号。例如:psql -h localhost -p 5433 -U yourusername -d yourdatabase确保新的端口没有被其他服务占用。你可以使用 netstat 或 lsof 命令来检查端口的使用情况。例如,在 Linux 上:sudo netstat -tuln | grep 5433 或sudo lsof -i :5433这样,你就可以成功更改并使用新的 PostgreSQL 端口号了。如果你只是想临时测试而不重启服务,也可以直接在命令行中使用 -p 参数指定端口号进行连接。例如:psql -h localhost -p 5433 -U yourusername -d yourdatabase这将允许你连接到使用新端口号的 PostgreSQL 实例。
  • [技术干货] PostgreSQL 调整临时表存储空间
    PostgreSQL 调整临时表存储空间在 PostgreSQL 中,临时表(包括显式创建的临时表和查询过程中生成的临时数据)的存储空间主要通过以下几个参数进行配置:主要配置参数temp_buffers控制每个数据库会话使用的临时缓冲区大小默认值通常为 8MB只在会话开始时设置有效,之后修改不会影响当前会话示例:SET temp_buffers = '64MB';work_mem控制内部排序操作和哈希表使用的内存量影响临时文件生成前的内存使用量默认值通常为 4MB示例:SET work_mem = '16MB';maintenance_work_mem维护操作(如VACUUM、CREATE INDEX等)使用的内存间接影响临时表操作默认值通常为 64MBtemp_file_limit限制会话可以使用的临时文件总量默认值为 -1(无限制)示例:SET temp_file_limit = '1GB';临时表存储位置PostgreSQL 的临时文件通常存储在:数据目录的 base/pgsql_tmp 子目录中或由 temp_tablespaces 参数指定的表空间中配置临时表空间可以指定专门的表空间用于临时文件:-- 设置临时表空间(可以指定多个,循环使用) SET temp_tablespaces = 'temp_tablespace1,temp_tablespace2'; -- 或者在postgresql.conf中设置 temp_tablespaces = 'temp_tablespace1' 监控临时表使用查看当前会话的临时文件使用情况:SELECT * FROM pg_stat_database WHERE datname = current_database(); 查看临时文件生成情况:SELECT * FROM pg_statio_user_tables WHERE schemaname = 'pg_temp'; 在日志中记录临时文件生成(在postgresql.conf中):log_temp_files = 0 -- 记录所有临时文件生成最佳实践对于大量临时表操作,适当增加 work_mem 可以避免使用磁盘临时文件为临时表创建专门的表空间,最好使用快速的存储设备监控临时文件生成,优化查询以减少临时表使用在会话开始时设置 temp_buffers,因为之后修改无效示例配置调整-- 在会话开始时设置 SET temp_buffers = '128MB'; SET work_mem = '32MB'; SET temp_tablespaces = 'fast_temp_space'; -- 或者在postgresql.conf中全局设置 ALTER SYSTEM SET work_mem = '32MB'; ALTER SYSTEM SET temp_buffers = '64MB'; 通过合理调整这些参数,可以显著提高涉及大量临时表操作的查询性能。
  • [技术干货] postgress查看临时表
    在PostgreSQL中,在临时表(temporary tables)的使用上,你可以创建两种类型的临时表:局部临时表(local temporary tables)和全局临时表(global temporary tables)。1. 局部临时表局部临时表只在创建它们的会话中可见,并且当该会话结束时,表会自动被删除。创建局部临时表时,需要在表名前加上CREATE TEMPORARY关键字。示例:‌sqlCopy CodeCREATE TEMPORARY TABLE temp_table (    id SERIAL PRIMARY KEY,    data TEXT);在这个例子中,temp_table是一个局部临时表,只能在当前数据库会话中访问。当会话结束时(例如,客户端断开连接),temp_table会自动被删除。2. 全局临时表全局临时表在所有数据库会话中都是可见的,但是它们只在创建它们的数据库会话中可以被写入。当第一个创建该表的会话结束时,表及其数据会被删除。其他会话仍然可以访问这个表,但不能写入数据。创建全局临时表时,需要在表名前加上CREATE TEMPORARY TABLE ON COMMIT关键字。示例:‌sqlCopy CodeCREATE TEMPORARY TABLE global_temp_table ON COMMIT DELETE ROWS (    id SERIAL PRIMARY KEY,    data TEXT);在这个例子中,global_temp_table是一个全局临时表,它在所有会话中可见,但只有在创建它的会话中可以写入数据。当创建这个表的会话结束时,表中的数据会被删除,但表结构仍然存在直到数据库重启或者直到执行DROP TABLE命令。注意事项:性能‌:使用临时表可以提高查询性能,尤其是在处理大量数据和需要多次访问相同数据集的情况下。持久性‌:局部临时表只在当前会话中存在,而全局临时表则在第一个创建它的会话结束时删除数据,但在整个数据库生命周期内保持结构不变。权限‌:临时表遵循与普通表相同的权限规则,即用户需要有相应的权限才能访问或修改这些表。清理‌:虽然PostgreSQL在会话结束时会自动清理局部临时表,但在长时间运行的脚本或应用程序中,最好明确地在不需要时显式地删除临时表以避免潜在的资源占用问题。可以使用DROP TABLE IF EXISTS temp_table;来安全地删除临时表。使用临时表是处理大量数据或在多个查询步骤中重用相同数据集时的有效策略。
  • [技术干货] 11月技术干货合集
    11月技术干货合集分享:1、 Linux使用waitpid回收多个子进程的方法小结 — 转载cid:link_02、 Linux获取子进程退出值和异常终止信号的完整指南 — 转载cid:link_1PostgreSQL 安装部署及配置使用教程 — 转载cid:link_34、 MongoDB分片模式集群部署方案详解 — 转载cid:link_4redis缓存神器之@Cacheable注解详解 — 转载cid:link_55、通过Redisson监听Redis集群的Key过期事件的实现指南 — 转载cid:link_66、Oracle数据库空间回收从诊断到优化实战指南详细教程 — 转载cid:link_77、MySQL EXPLAIN详细解析 — 转载cid:link_88、Linux使用wait函数回收子进程的操作指南 — 转载cid:link_99、Nginx 中的Rewrite 使用示例详解 — 转载cid:link_1010、 Linux系统日志持久化配置的完整指南 — 转载cid:link_1111、 PostgreSQL中pg_surgery的扩展使用 — 转载cid:link_1212、PostgreSQL扩展bloom的具体使用 — 转载cid:link_1313、PostgreSQL扩展UUID-OSSP的使用方法 — 转载cid:link_214、MySQL深度分页优化的常用策略 — 转载cid:link_1415、Linux使用du和sort命令查找最大文件和目录 — 转载https://bbs.huaweicloud.com/forum/thread-0250198135428299001-1-1.html
  • [技术干货] PostgreSQL扩展UUID-OSSP的使用方法 — 转载
    一、扩展概述UUID-OSSP 扩展提供了多种 UUID 生成函数,支持以下几种 UUID 版本:UUID v1:基于 MAC 地址和时间戳UUID v3:基于 MD5 哈希和命名空间UUID v4:基于随机数UUID v5:基于 SHA-1 哈希和命名空间二、安装扩展1. 安装方法12-- 安装扩展CREATE EXTENSION IF NOT EXISTS "uuid-ossp";2. 验证安装12345-- 查看已安装扩展SELECT * FROM pg_available_extensions WHERE name = 'uuid-ossp'; -- 查看提供的函数\df uuid_*三、核心函数说明1. 生成 UUID v4 (随机)12SELECT uuid_generate_v4();-- 示例输出: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a112. 生成 UUID v1 (基于时间戳和 MAC)12SELECT uuid_generate_v1();-- 示例输出: 6ba7b810-9dad-11d1-80b4-00c04fd430c83. 生成命名空间 UUID (v3 和 v5)123456-- 使用预定义命名空间SELECT uuid_generate_v3(uuid_ns_url(), 'https://example.com');SELECT uuid_generate_v5(uuid_ns_oid(), '1.3.6.1.4.1.343'); -- 自定义命名空间SELECT uuid_generate_v5('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'my-data');4. 预定义命名空间常量1234SELECT uuid_ns_url();    -- 用于URL的命名空间UUIDSELECT uuid_ns_dns();    -- 用于DNS的命名空间UUIDSELECT uuid_ns_oid();    -- 用于ISO OID的命名空间UUIDSELECT uuid_ns_x500();   -- 用于X.500 DN的命名空间UUID四、实际应用示例1. 作为表的主键12345678CREATE TABLE users (    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),    username VARCHAR(50) NOT NULL,    email VARCHAR(100)); -- 插入数据时会自动生成UUIDINSERT INTO users (username, email) VALUES ('johndoe', 'john@example.com');2. 批量生成UUID12-- 生成10个随机UUIDSELECT uuid_generate_v4() FROM generate_series(1,10);3. 与其他数据类型转换12345-- UUID转字符串SELECT uuid_generate_v4()::TEXT; -- 字符串转UUIDSELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID;五、性能考虑UUID v4 是最常用的版本,性能最好UUID v1 包含时间戳,适合需要时间顺序的场景UUID v3/v5 是确定性的,相同输入总是产生相同输出六、与其他数据库的对比特性PostgreSQL (uuid-ossp)MySQLSQL Server安装方式扩展模块内置函数内置函数UUID版本支持v1, v3, v4, v5仅UUID()函数(类似v1)NEWID()(类似v4), NEWSEQUENTIALID()命名空间支持有无无性能优秀良好优秀七、常见问题解决扩展安装失败:12# 可能需要安装contrib包sudo apt-get install postgresql-contrib权限问题:12-- 授予普通用户使用权限GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO app_user;版本兼容性:PostgreSQL 13+ 内置了gen_random_uuid()函数(类似uuid v4)但uuid-ossp提供更全面的UUID生成选项八、最佳实践建议生产环境推荐使用UUID v4:12-- 比v1更安全(不暴露MAC地址)ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuid_generate_v4();索引优化:12-- 为UUID列创建索引CREATE INDEX idx_users_id ON users(id); 分布式系统:UUID非常适合分布式数据库环境避免了自增ID的同步问题UUID-OSSP扩展为PostgreSQL提供了强大的全局唯一标识符生成能力,特别适合需要跨系统唯一标识的场景。
  • [技术干货] PostgreSQL扩展bloom的具体使用 — 转载
    一、扩展概述bloom 是 PostgreSQL 提供的一个基于布隆过滤器(Bloom Filter)的索引扩展,特别适合多列任意组合查询的优化场景。二、核心特性特性描述优势多列索引单索引支持多列组合减少索引数量模糊匹配高效处理=和IN查询优于B-tree多列索引空间效率使用概率数据结构比传统索引更紧凑快速排除可确定"绝对不存在"减少磁盘I/O三、安装启用12345-- 安装扩展CREATE EXTENSION bloom; -- 验证安装SELECT extname, extversion FROM pg_extension WHERE extname = 'bloom';四、索引创建语法基本形式12CREATE INDEX index_name ON table_name USING bloom (col1, col2, ...)WITH (length=..., col1=..., col2=...);参数说明参数描述默认值length每个签名的长度(位)80colN每列的位数2false_positive目标误报率0.01五、实际应用示例1. 创建Bloom索引1234-- 在用户表上创建多列bloom索引CREATE INDEX users_bloom_idx ON users USING bloom (first_name, last_name, email, department)WITH (length=100, first_name=5, last_name=5, email=6, department=3);2. 查询使用1234567-- 多列组合查询EXPLAIN ANALYZE SELECT * FROM users WHERE first_name = 'John' AND department = 'Engineering'; -- IN列表查询EXPLAIN ANALYZE SELECT * FROM users WHERE email IN ('a@example.com', 'b@example.com');六、性能对比与B-tree索引比较场景Bloom索引B-tree索引多列AND查询⭐⭐⭐⭐⭐⭐单列精确查询⭐⭐⭐⭐⭐⭐存储空间⭐⭐⭐⭐⭐更新性能⭐⭐⭐⭐⭐七、配置优化1. 参数调优原则1234-- 根据数据特征调整CREATE INDEX optimized_bloom_idx ON large_table USING bloom (col1, col2, col3)WITH (length=200, col1=4, col2=4, col3=4, false_positive=0.005);2. 计算公式1234位数选择 ≈ -n·ln(p) / (ln(2))²其中:n = 预计唯一值数量p = 可接受的误报率八、适用场景数据分析系统多维度任意组合筛选数据仓库查询日志处理多字段联合查询高基数维度查询用户目录姓名/邮箱/部门等组合搜索九、限制与注意事项功能限制:仅支持等值查询(=, IN)不支持范围查询(>, <)不支持排序存储考虑:索引大小随列数线性增长每列位数增加会提升精度但增大索引误报处理:12345-- 实际查询应处理可能的误报SELECT * FROM users WHERE first_name = 'John' AND department = 'Engineering'AND first_name_bloom = 'John'  -- 使用索引列AND department_bloom = 'Engineering';十、维护操作1. 重建索引1REINDEX INDEX users_bloom_idx;2. 监控使用情况12SELECT * FROM pg_stat_user_indexes WHERE indexrelname = 'users_bloom_idx';bloom扩展为PostgreSQL提供了处理多列组合查询的高效方式,特别适合需要灵活查询但不需要排序的场景。合理配置可在空间和性能间取得最佳平衡。
  • [技术干货] PostgreSQL中pg_surgery的扩展使用 — 转载
    pg_surgery 是 PostgreSQL 的一个特殊扩展,它提供了一系列"手术式"函数,用于在极端情况下修复损坏的数据库。这个扩展包含了一些高风险操作,应仅由经验丰富的数据库管理员在别无选择的情况下使用。一、扩展概述核心功能修复损坏的表和索引强制修改系统目录恢复无法通过常规方法访问的数据处理事务ID回卷问题适用场景数据库损坏且无法通过常规恢复方法修复时系统目录不一致导致数据库无法启动时需要绕过正常约束进行紧急修复时风险警告⚠️ 这些操作可能破坏数据完整性⚠️ 操作前必须进行完整备份⚠️ 仅应在专业指导下使用二、安装与启用12345-- 安装扩展CREATE EXTENSION pg_surgery; -- 验证安装SELECT * FROM pg_available_extensions WHERE name = 'pg_surgery';三、核心功能函数1. 堆表修复函数heap_force_kill(regclass, tid[])强制将指定的元组标记为已删除12-- 修复包含损坏元组的表SELECT heap_force_kill('my_table'::regclass, ARRAY['(0,1)']::tid[]);heap_force_freeze(regclass, tid[])强制冻结指定元组的事务ID12-- 处理事务ID回卷问题SELECT heap_force_freeze('problem_table'::regclass, ARRAY['(0,1)']::tid[]);2. 索引修复函数btree_force_options(index regclass, options text[])强制设置B-tree索引选项12-- 修复损坏的B-tree索引SELECT btree_force_options('my_index'::regclass, ARRAY['fastupdate=off']);3. 事务状态修复txid_force_status(txid bigint, status text)强制修改事务状态12-- 将卡住的事务标记为已提交SELECT txid_force_status(123456, 'committed');四、使用场景与示例场景1:修复损坏的表元组12345678-- 1. 首先识别损坏的元组SELECT ctid, * FROM my_table WHERE ...; -- 返回错误 -- 2. 强制删除损坏元组SELECT heap_force_kill('my_table'::regclass, ARRAY['(0,1)']::tid[]); -- 3. 重建表VACUUM FULL my_table;场景2:处理事务ID回卷1234567891011121314-- 1. 识别需要冻结的元组SELECT ctid, xmin, xmax FROM problem_table WHERE age(xmin) > 2000000000; -- 2. 强制冻结这些元组SELECT heap_force_freeze(  'problem_table'::regclass,  ARRAY(SELECT ctid::text::tid         FROM problem_table         WHERE age(xmin) > 2000000000)); -- 3. 执行常规VACUUMVACUUM problem_table;场景3:修复无法启动的数据库12345-- 在单用户模式下使用:postgres --single -D /path/to/data/directory dbname -- 修复系统目录不一致SELECT pg_surgery_function(...);五、安全注意事项必须备份:执行任何操作前进行完整物理备份1pg_basebackup -D /backup/location -X stream操作审计:记录所有手术操作12CREATE TABLE surgery_audit ASSELECT now(), current_user, * FROM pg_surgery_function(...);权限控制:严格限制访问12REVOKE ALL ON FUNCTION heap_force_kill FROM PUBLIC;GRANT EXECUTE ON FUNCTION heap_force_kill TO dbadmin;六、与其他工具对比工具/方法适用场景风险等级技术要求pg_surgery极端损坏情况非常高专家级pg_resetwalWAL损坏高高级pg_dump/restore逻辑损坏中中级常规VACUUM一般维护低初级七、最佳实践建议先尝试常规方法:REINDEX 尝试修复索引VACUUM FULL 尝试修复表从备份恢复测试环境验证:先在测试环境验证手术操作评估操作影响操作后检查:123456-- 检查表完整性ANALYZE repaired_table; -- 验证索引SELECT indexrelid::regclass, indisvalid FROM pg_index WHERE indrelid = 'repaired_table'::regclass;长期监控:操作后加强监控定期检查修复对象的状态pg_surgery 是PostgreSQL的"最后手段"工具,它提供了在极端情况下挽救数据的能力,但代价是潜在的数据完整性风险。合理使用这一扩展可以避免灾难性数据丢失,但必须谨慎行事,并充分理解其后果。
  • [技术干货] PostgreSQL 安装部署及配置使用教程 — 转载
    一、PostgreSQL 简介PostgreSQL 是一个功能强大的开源对象关系型数据库系统,支持 SQL 标准和扩展,适合各种规模应用。二、安装 PostgreSQL1. Linux(以 Ubuntu 为例)1.1 更新系统包列表1sudo apt update1.2 安装 PostgreSQL1sudo apt install postgresql postgresql-contrib1.3 查看 PostgreSQL 服务状态1sudo systemctl status postgresql2. CentOS/RHEL1234sudo yum install -y postgresql-server postgresql-contribsudo postgresql-setup initdbsudo systemctl enable postgresqlsudo systemctl start postgresql3. Windows访问PostgreSQL 官方下载安装包。运行安装程序,按提示选择安装路径、端口、超级用户密码等。安装完成后,可通过 pgAdmin 或命令行管理数据库。三、基本配置1. 配置文件位置postgresql.conf:主配置文件,通常在 /etc/postgresql/<版本>/main/ 或 /var/lib/pgsql/data/pg_hba.conf:客户端连接认证配置文件2. 修改监听地址(允许远程连接)编辑 postgresql.conf:1listen_addresses = '*'编辑 pg_hba.conf,添加如下一行允许所有 IP 通过密码方式访问:1host    all             all             0.0.0.0/0               md5修改后重启服务:1sudo systemctl restart postgresql3. 设置端口号在 postgresql.conf 中修改:1port = 5432四、数据库管理1. 切换到 PostgreSQL 用户(Linux)1sudo -i -u postgres2. 进入数据库命令行psql3. 创建数据库和用户123456-- 创建用户CREATE USER myuser WITH PASSWORD 'mypassword';-- 创建数据库CREATE DATABASE mydb OWNER myuser;-- 授权GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser;4. 修改用户权限1ALTER USER myuser WITH SUPERUSER;五、基本使用1. 连接数据库命令行连接1psql -U myuser -h localhost -d mydb使用 pgAdmin(图形化工具)打开 pgAdmin,添加服务器,填写连接信息。2. 常用 SQL 操作1234567891011121314-- 创建表CREATE TABLE users (    id SERIAL PRIMARY KEY,    name VARCHAR(50),    email VARCHAR(100));-- 插入数据INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');-- 查询数据SELECT * FROM users;-- 更新数据UPDATE users SET name = 'Bob' WHERE id = 1;-- 删除数据DELETE FROM users WHERE id = 1;六、备份与恢复1. 备份数据库1pg_dump -U myuser -h localhost mydb > mydb_backup.sql2. 恢复数据库1psql -U myuser -h localhost -d mydb < mydb_backup.sql七、常用维护命令查看数据库列表:\l切换数据库:\c dbname查看表列表:\dt查看用户列表:\du退出:\q八、附加说明推荐使用pgAdmin进行图形化管理。可以使用 Docker 快速部署:1docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres九、进阶配置1. 连接数与内存参数在 postgresql.conf 中调整:12345max_connections = 100          # 最大连接数shared_buffers = 128MB         # 数据库缓存区大小,建议为物理内存的 1/4work_mem = 4MB                 # 每个查询操作分配的内存maintenance_work_mem = 64MB    # 维护操作(如 VACUUM)的内存effective_cache_size = 512MB   # 操作系统可用于缓存的内存估算修改后需重启 PostgreSQL 服务。2. 日志设置1234logging_collector = onlog_directory = 'pg_log'log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'log_statement = 'all'   # 建议生产环境设置为 'none' 或 'mod'3. 自动清理(VACUUM)PostgreSQL 自动进行垃圾回收,但可手动执行:12VACUUM;VACUUM FULL;      -- 更彻底,可能会锁表十、性能优化建议索引优化创建索引可加速查询:1CREATE INDEX idx_users_email ON users(email);查询优化使用 EXPLAIN 分析 SQL 性能:1EXPLAIN SELECT * FROM users WHERE email = 'xxx';分区表大表可分区提升性能:123456CREATE TABLE measurement (  city_id         int,  logdate         date,  peaktemp        int,  unitsales       int) PARTITION BY RANGE (logdate);连接池建议使用连接池减少资源消耗。十一、扩展插件PostGIS:地理空间数据库扩展12sudo apt install postgisCREATE EXTENSION postgis;uuid-ossp:生成 UUID12CREATE EXTENSION "uuid-ossp";SELECT uuid_generate_v4();pg_stat_statements:SQL 统计分析12CREATE EXTENSION pg_stat_statements;SELECT * FROM pg_stat_statements;十二、安全加固密码策略强密码,定期更换,禁用默认用户 postgres 的远程访问。限制访问 IP编辑 pg_hba.conf,只允许信任 IP 段访问。SSL 加密配置 SSL,保护数据传输安全。123ssl = onssl_cert_file = 'server.crt'ssl_key_file = 'server.key'定期备份使用 pg_dump 或 pg_basebackup 定期备份,保存到安全位置。十三、常见问题及解决方法无法远程连接检查 listen_addresses 和 pg_hba.conf 配置。检查防火墙是否开放 5432 端口。密码认证失败确认用户名、密码正确。检查 pg_hba.conf 的认证方式(如 md5、scram-sha-256)。数据库启动失败查看日志文件(pg_log 目录)。检查磁盘空间和配置文件语法。性能变慢检查慢查询日志。执行 VACUUM 和 REINDEX。增加内存分配。十四、高可用与主从复制1. 主从流复制主库配置编辑 postgresql.conf:1234wal_level = replicamax_wal_senders = 10archive_mode = onarchive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'在 pg_hba.conf 添加从库 IP:1host replication all <slave_ip>/32 md5从库初始化使用 pg_basebackup:1pg_basebackup -h <master_ip> -D /var/lib/postgresql/data -U replication -P --wal-method=stream配置 recovery.conf(PostgreSQL 12 及以上为 standby.signal 文件)。十五、常用工具推荐pgAdmin:官方图形化管理工具DBeaver:多数据库管理工具Navicat:商业数据库管理工具psql:命令行工具PgBouncer:轻量级连接池十六、自动备份与定时任务1. 使用cron定时备份(Linux)编辑 crontab,例如每天凌晨2点自动备份数据库:1crontab -e添加如下内容:10 2 * * * pg_dump -U myuser -h localhost mydb > /backup/mydb_$(date +\%F).sql注意:确保 /backup/ 目录存在且有写权限,myuser 用户有备份权限。2. 自动清理过期备份脚本示例保留最近7天备份,其余自动删除:1find /backup/ -name "mydb_*.sql" -mtime +7 -exec rm {} \;3. Windows 定时任务可用“任务计划程序”调用批处理或 PowerShell 脚本实现定时备份。十七、数据库监控1. 内置视图pg_stat_activity:查看当前连接和执行的SQLpg_stat_database:数据库级统计信息示例:12SELECT * FROM pg_stat_activity;SELECT datname, numbackends, xact_commit, xact_rollback FROM pg_stat_database;2. 监控工具pgAdmin:自带部分监控功能Prometheus + Grafana:结合postgres_exporter进行可视化监控Zabbix、Nagios 等企业级监控工具十八、升级与迁移1. 版本升级1.1 使用pg_upgrade工具(推荐大版本升级)安装新版本 PostgreSQL停止旧服务,备份数据执行 pg_upgrade检查数据完整性,切换新服务1.2 逻辑备份还原(跨平台、跨版本)123pg_dumpall -U postgres > all.sql# 在新环境还原psql -U postgres -f all.sql2. 数据迁移跨服务器迁移:使用 pg_dump + psql,或 pg_basebackup(物理迁移)跨数据库类型:可用DBConvert等工具十九、常用SQL脚本示例1. 批量插入数据12345INSERT INTO users (name, email)VALUES  ('Tom', 'tom@example.com'),  ('Jerry', 'jerry@example.com'),  ('Spike', 'spike@example.com');2. 批量更新1UPDATE users SET status = 'active' WHERE id IN (1,2,3);3. 查询表结构1\d+ users4. 查询某表空间大小1SELECT pg_size_pretty(pg_total_relation_size('users'));5. 查询数据库当前连接数1SELECT count(*) FROM pg_stat_activity;6. 查看锁等待1SELECT * FROM pg_locks WHERE NOT granted;二十、PostgreSQL 优秀学习资源·PostgreSQL 官方文档·Postgres中文社区·PostgreSQL 练习平台·LeetCode SQL 练习 二十一、FAQ 补充如何重置用户密码?1ALTER USER myuser WITH PASSWORD 'newpassword';如何查看数据库版本?1SELECT version();如何导出/导入表结构而不带数据?1pg_dump -U myuser -h localhost -s mydb > mydb_schema.sql如何只导出/导入部分表?12pg_dump -U myuser -h localhost -t users mydb > users.sqlpsql -U myuser -d mydb < users.sql二十三、表分区(Partition)1. 适用场景大数据量表,按时间、ID等字段分区,提高查询与维护效率。2. 分区创建示例(按日期分区)1234567CREATE TABLE sales (    id serial PRIMARY KEY,    sale_date date NOT NULL,    amount numeric) PARTITION BY RANGE (sale_date);CREATE TABLE sales_2023 PARTITION OF sales FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');CREATE TABLE sales_2024 PARTITION OF sales FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');3. 查看分区信息12345SELECT    inhrelid::regclass AS child,    inhparent::regclass AS parentFROM    pg_inherits;二十四、事务与并发控制1. 事务基础1234BEGIN;UPDATE users SET balance = balance - 100 WHERE id = 1;UPDATE users SET balance = balance + 100 WHERE id = 2;COMMIT;用于保证操作的原子性,一致性,隔离性,持久性(ACID)。2. 隔离级别READ COMMITTEDREPEATABLE READSERIALIZABLE设置示例:1SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;3. 并发与锁行级锁:SELECT ... FOR UPDATE表级锁:LOCK TABLE table_name IN ACCESS EXCLUSIVE MODE;查看锁信息:1SELECT * FROM pg_locks WHERE NOT granted;二十五、触发器与存储过程1. 触发器示例自动记录更新日志:12345678910111213141516CREATE TABLE users_log (    id serial PRIMARY KEY,    user_id int,    action varchar(20),    log_time timestamp DEFAULT now());CREATE OR REPLACE FUNCTION log_user_update()RETURNS trigger AS $$BEGIN    INSERT INTO users_log(user_id, action) VALUES (NEW.id, 'update');    RETURN NEW;END;$$ LANGUAGE plpgsql;CREATE TRIGGER user_update_trigAFTER UPDATE ON usersFOR EACH ROW EXECUTE FUNCTION log_user_update();2. 存储过程(函数)示例123456CREATE OR REPLACE FUNCTION add_user(name text, email text)RETURNS void AS $$BEGIN    INSERT INTO users(name, email) VALUES (name, email);END;$$ LANGUAGE plpgsql;调用:1SELECT add_user('Bob', 'bob@example.com');二十六、JSON 与全文搜索1. JSON 数据类型123456CREATE TABLE orders (    id serial PRIMARY KEY,    info jsonb);INSERT INTO orders(info) VALUES ('{"customer": "Tom", "items": ["apple", "banana"]}');SELECT info->>'customer' FROM orders;2. 全文搜索123CREATE TABLE docs (id serial PRIMARY KEY, content text);INSERT INTO docs(content) VALUES ('PostgreSQL is a powerful database system.');SELECT * FROM docs WHERE to_tsvector('english', content) @@ to_tsquery('powerful & database');二十七、常用运维脚本1. 清理无用连接123SELECT pg_terminate_backend(pid)FROM pg_stat_activityWHERE state = 'idle' AND pid <> pg_backend_pid();2. 自动重建索引1REINDEX TABLE users;3. 检查表膨胀123SELECT relname, n_dead_tupFROM pg_stat_user_tablesWHERE n_dead_tup > 1000;二十八、故障处理方法1. 数据库无法启动检查日志文件(默认在 pg_log 目录)检查磁盘空间、配置文件语法使用 pg_resetwal 修复 WAL 日志损坏2. 数据丢失恢复立即停止服务,备份现有数据目录通过备份文件和 WAL 日志恢复3. 锁表导致阻塞查询阻塞 SQL杀死阻塞进程(注意数据一致性)二十九、集群与分布式方案1. 主从复制(同步/异步)见前文说明。2. 读写分离主库写,从库读应用层或中间件(如 Pgpool-II)实现3. 分布式扩展Citus:PostgreSQL 分布式扩展插件,适合大数据分表分库Greenplum:基于 PostgreSQL 的分布式数据仓库4. 高可用Patroni:自动化主从切换、故障恢复Keepalived + VIP:实现数据库虚拟 IP 高可用三十、PostgreSQL 版本与生命周期官方建议使用长期支持版本(LTS)定期关注官方发布页面获取安全补丁和新版本信息
  • [数据库类] postgres数据库,为什么间隔性的出现一个进程curl -fksSLA-
    第一次用postgres数据库,为什么间隔性的出现一个以curl 开始的进程curl -fksSLA- 。偶尔出现、消失。还总是告警,什么情况如下:
  • [技术干货] PostgreSQL扩展UUID-OSSP的使用方法 —转载
    一、扩展概述UUID-OSSP 扩展提供了多种 UUID 生成函数,支持以下几种 UUID 版本:UUID v1:基于 MAC 地址和时间戳UUID v3:基于 MD5 哈希和命名空间UUID v4:基于随机数UUID v5:基于 SHA-1 哈希和命名空间二、安装扩展1. 安装方法12-- 安装扩展CREATE EXTENSION IF NOT EXISTS "uuid-ossp";2. 验证安装12345-- 查看已安装扩展SELECT * FROM pg_available_extensions WHERE name = 'uuid-ossp'; -- 查看提供的函数\df uuid_*三、核心函数说明1. 生成 UUID v4 (随机)12SELECT uuid_generate_v4();-- 示例输出: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a112. 生成 UUID v1 (基于时间戳和 MAC)12SELECT uuid_generate_v1();-- 示例输出: 6ba7b810-9dad-11d1-80b4-00c04fd430c83. 生成命名空间 UUID (v3 和 v5)123456-- 使用预定义命名空间SELECT uuid_generate_v3(uuid_ns_url(), 'https://example.com');SELECT uuid_generate_v5(uuid_ns_oid(), '1.3.6.1.4.1.343'); -- 自定义命名空间SELECT uuid_generate_v5('a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 'my-data');4. 预定义命名空间常量1234SELECT uuid_ns_url();    -- 用于URL的命名空间UUIDSELECT uuid_ns_dns();    -- 用于DNS的命名空间UUIDSELECT uuid_ns_oid();    -- 用于ISO OID的命名空间UUIDSELECT uuid_ns_x500();   -- 用于X.500 DN的命名空间UUID四、实际应用示例1. 作为表的主键12345678CREATE TABLE users (    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),    username VARCHAR(50) NOT NULL,    email VARCHAR(100)); -- 插入数据时会自动生成UUIDINSERT INTO users (username, email) VALUES ('johndoe', 'john@example.com');2. 批量生成UUID12-- 生成10个随机UUIDSELECT uuid_generate_v4() FROM generate_series(1,10);3. 与其他数据类型转换12345-- UUID转字符串SELECT uuid_generate_v4()::TEXT; -- 字符串转UUIDSELECT 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID;五、性能考虑UUID v4 是最常用的版本,性能最好UUID v1 包含时间戳,适合需要时间顺序的场景UUID v3/v5 是确定性的,相同输入总是产生相同输出六、与其他数据库的对比特性PostgreSQL (uuid-ossp)MySQLSQL Server安装方式扩展模块内置函数内置函数UUID版本支持v1, v3, v4, v5仅UUID()函数(类似v1)NEWID()(类似v4), NEWSEQUENTIALID()命名空间支持有无无性能优秀良好优秀七、常见问题解决扩展安装失败:12# 可能需要安装contrib包sudo apt-get install postgresql-contrib权限问题:12-- 授予普通用户使用权限GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO app_user;版本兼容性:PostgreSQL 13+ 内置了gen_random_uuid()函数(类似uuid v4)但uuid-ossp提供更全面的UUID生成选项八、最佳实践建议生产环境推荐使用UUID v4:12-- 比v1更安全(不暴露MAC地址)ALTER TABLE orders ALTER COLUMN id SET DEFAULT uuid_generate_v4();索引优化:12-- 为UUID列创建索引CREATE INDEX idx_users_id ON users(id);分布式系统:UUID非常适合分布式数据库环境避免了自增ID的同步问题
  • [案例共创] 【案例共创】远程开发环境中部署Bun + Next.js + tRPC与开发者空间生态版GaussDB实践操作并编写学生成绩管理系统
    余年方十七,习编程已有数载,虽非初学,然深知学无止境,技艺之精进,如逆水行舟,不进则退。 技艺渐精 昔日初学时,常为类型错误而苦恼,今已能熟练运用多种语言。ts之强大,Python之优雅,前端三剑客之灵动,后端框架之稳健,皆有所涉猎。数据结构与算法,亦略有心得,排序查找,动态规划,虽不敢言精通,然基本功底尚算扎实。 项目实战 近年来,陆续完成数个项目:网站开发、移动应用、数据分析工具等。每一次从需求分析到架构设计,从编码实现到测试部署,皆亲力亲为。虽过程艰辛,然收获颇丰。始知纸上谈兵易,实际开发难。用户体验、性能优化、代码维护,诸般考量,方显编程之深奥。 开源贡献 偶有小作,贡献于开源社区,虽微不足道,然能为他人所用,心甚慰藉。GitHub上星标虽少,然每一个fork,每一个issue,皆是对余之鞭策与鼓励。 技术视野 关注前沿技术,人工智能、区块链、云计算等,皆有所了解。深知技术日新月异,昨日之新技术,今日或已过时。唯有保持学习之心,方能不被时代所抛弃。 反思与展望 回首来路,虽有所成,然亦深感不足。代码质量有待提升,系统设计能力尚需磨练,团队协作经验亦显匮乏。他日若能进入知名企业,与高手切磋,定能更上一层楼。 夫编程者,匠心独运之艺也。既要有扎实之基础,又需具创新之思维。余虽年少,然志在千里,愿以代码改变世界,以技术服务社会。作者写此文时正值十七岁生日前夕,虽习编程数年,然经验尚浅,见解有限,往有不足之处,请在评论区指出。案例简介:本案例选择bun + Next.js作为示例编写了学生成绩管理系统,并借助开发者空间云开发环境提供的免费 GaussDB数据库和HCE2.0开发环境进行本地部署Next.js生态组件、轻松部署上云,直观地展示在实际应用开发中为开发者带来的便利。一、概述1.1 案例介绍Bun + Next.js + tRPC 是一个现代化的全栈 TypeScript Web 开发技术栈,可以快速开发高性能和可维护的网站。Bun 作为超快的 JavaScript 运行时和包管理器,Next.js 负责处理前端和后端开发中的复杂部分,tRPC 提供端到端类型安全的 API 层。在数据库操作方面,本技术栈支持直接使用原生 SQL 语句进行数据库交互,无需额外的 ORM 抽象层,既保证了查询性能的最优化,又提供了最大的灵活性。让开发者可以专注于编写业务逻辑,而无需重复造轮子。本案例借助开发者空间云开发环境部署 Next.js 项目,通过 tRPC 构建类型安全的 API 接口,并直接使用 SQL 语句与开发者空间提供的免费 GaussDB 数据库实例对接,完成一个现代化 Web 应用构建。通过实际操作,让大家深入了解如何利用 Bun + Next.js + tRPC + 原生 SQL 技术栈开发并部署一个高性能的全栈 Web 应用。华为开发者空间,是为全球开发者打造的专属开发者空间,致力于为每位开发者提供一台 云开发环境、一套开发工具和云上存储空间,汇聚昇腾、鸿蒙、鲲鹏、GaussDB、欧拉 等华为各项根技术的开发工具资源,并提供配套案例指导开发者 从开发编码到应用调 测,基于华为根技术生态高效便捷的知识学习、技术体验、应用创新。1.2 适用对象企业个人开发者初高中学生和高校学生1.3 案例时间本案例总时长预计120分钟。1.4 案例流程本案例将通过三个主要步骤,带领大家在华为云开发者空间上构建一个完整的 Bun + Next.js + tRPC + 原生 SQL 全栈应用:第一步:申请华为云开发者空间的 GaussDB 数据库注册并登录华为云开发者空间申请免费的 GaussDB 数据库实例获取数据库连接配置信息第二步:进行华为云开发者空间的云开发环境进行 Bun + Next.js + tRPC 安装与配置创建云开发环境实例安装 Bun 运行时环境初始化 Next.js TypeScript 项目集成 tRPC 框架,配置端到端类型安全的 API设置数据库连接和环境变量第三步:启用 Next.js Web 服务,适配 GaussDB,在客户端测试原生 SQL 的建表和数据操作编写原生 SQL 建表脚本通过 tRPC 封装数据库操作接口启动 Next.js 开发服务器在客户端测试数据库的增删改查功能验证端到端类型安全和性能表现通过这三个步骤,您将完整体验从环境搭建到应用部署的全流程,深入理解 Bun + Next.js + tRPC + 原生 SQL 技术栈在华为云开发者空间上的强大能力。1.5 资源总览本案例预计花费0.8元。资源名称规格单价(元)时长(分钟)华为开发者空间 - 云开发环境鲲鹏通用计算增强型 kc1 2vCPUs 4G免费40华为开发者空间 - 生态版GaussDB单副本集中式版 4 vCPUs 16G HCE OS 64bit (200GB)免费40弹性公网IP按流量计费 5Mbit/s0.8元/GB40二、案例准备工作2.1 配置云开发环境根据案例《开发者空间 - 云开发环境使用指导》,请查看下面链接,配置云开发环境,并通过xshell等链接工具登录云开发环境。开发者空间 - 云开发环境使用指导2.2 领取GaussDB数据库免费领取GaussDB在线试用版(2025年 06月 21日 - 2025年 12月 31日)。华为开发者空间-GaussDB云数据库领取与使用指导注:部署的Django项目需要对接GaussDB,因此GaussDB需要绑定EIP,参考上述指导中领取部分第(5)步bunx create-next-app@latest2.3 验证云开发环境与GaussDB互通通过xshell或windows命令终端登录云开发环境,使用ping [ip]测试GaussDB能否连接,如下是通的2.4 新建数据库及用户修改dn:password_encryption_type字段为1。0表示采用md5方式对密码加密。1表示采用sha256和md5两种方式分别对密码加密,2表示采用sha256方式对密码加密,3表示采用sm3方式对密码加密。MD5加密算法安全性低,存在安全风险,不建议使用。点击右上角登录数据库管理界面创建数据库使用如下图配置。通过右上角登录GaussDB的SQL操作界面,用如下SQL创建Django链接的登录用户。create user myuser with sysadmin password 'GaussDB@123'; 三、Bun + Next.js + Prisma 安装配置指南3.1 安装 Bun# 全局安装 bun sudo npm install -g bun # 验证安装 bun --version # 检查安装位置 which bun3.2 创建 Next.js 项目# 交互式创建next.js项目 bunx create-next-app@latest看到如下输出即为成功创建Next.js项目3.3测试Next.js是否安装成功# 查看项目结构 ls -la # 启动开发服务器测试 bun dev # 在另一个终端测试(如果需要) curl http://localhost:3000 3.4 安装trpc步骤 1: 安装依赖bun add @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod superjson依赖说明@trpc/server - 服务器端 tRPC 核心@trpc/client - 客户端 tRPC 核心@trpc/react-query - React Query 集成@trpc/next - Next.js 集成@tanstack/react-query - 数据获取和缓存zod - 类型验证superjson - 序列化(支持 Date、BigInt 等)步骤 2: 创建服务器端配置创建 app/lib/trpc.tsimport { initTRPC } from '@trpc/server'; import superjson from 'superjson'; import { ZodError } from 'zod'; const t = initTRPC.create({ transformer: superjson, errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.issues : null, }, }; }, }); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; 配置说明transformer: superjson - 使用 SuperJSON 进行序列化errorFormatter - 自定义错误格式化,支持 Zod 验证错误createTRPCRouter - 创建路由的工厂函数publicProcedure - 公开的 API 过程(无需认证)步骤 3: 创建路由tRPC 路由(Router)是服务端定义 API 端点和业务逻辑的核心组件,它通过类型安全的方式组织和管理所有的远程过程调用,包括查询(query)、变更(mutation)和订阅(subscription),并自动生成 TypeScript 类型定义供客户端使用,实现了从后端到前端的完整类型推断和 API 结构管理。创建主路由 app/lib/routers/_app.tsimport { createTRPCRouter } from '../trpc'; export const appRouter = createTRPCRouter({ }); export type AppRouter = typeof appRouter; 步骤 4: 创建 API 路由处理创建 app/api/trpc/[trpc]/route.tsimport { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { appRouter } from '../../../lib/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext: () => ({}), }); export { handler as GET, handler as POST }; 说明使用 Next.js App Router 的 API 路由fetchRequestHandler 处理 HTTP 请求createContext 创建请求上下文(当前为空对象)步骤 5: 创建客户端配置创建 utils/trpc.tsimport { createTRPCReact } from '@trpc/react-query'; import { type AppRouter } from '../app/lib/routers/_app'; export const trpc = createTRPCReact<AppRouter>(); 创建独立客户端 utils/trpc-client.tsimport { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import superjson from 'superjson'; import { type AppRouter } from '../app/lib/routers/_app'; export const trpcClient = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: '/api/trpc', transformer: superjson, }), ], }); 步骤 6: 设置提供者创建 app/providers.tsx'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import React, { useState } from 'react'; import superjson from 'superjson'; import { trpc } from '../utils/trpc'; export function TRPCProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: '/api/trpc', transformer: superjson, }), ], }) ); return ( <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> </trpc.Provider> ); } tRPC Provider 是一个配置组件,用于在客户端应用中设置 tRPC 客户端实例,它负责配置 API 端点、管理请求状态和缓存,并提供端到端的 TypeScript 类型安全,让前端能够直接调用后端函数而无需手动定义 API 接口类型。步骤 7: 集成到应用更新 app/layout.tsximport type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { TRPCProvider } from "./providers"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} > <TRPCProvider>{children}</TRPCProvider> </body> </html> ); } 3.5 连接和初始化数据库并编写相关方法步骤1:安装sql相关依赖bun add pg dotenv bun add -d @types/bun @types/pg tsxdotenv 是一个零依赖的模块,它可以从 .env 文件中加载环境变量到 process.env 中。步骤2:设置数据库连接创建 .env# GaussDB 数据库配置 DB_HOST=your-gaussdb-host.com DB_PORT=3306 DB_USER=your-username DB_PASSWORD=your-password DB_NAME=your-database-name # 可选配置 DB_MAX_CONNECTIONS=20 说明DB_HOST就是你数据库IPDB_PORT为数据库端口默认是8000DB_USER和DB_PASSWORD就是你刚刚创建的用户的密码步骤3:编写数据库相关代码创建 scripts/init-database.ts 用来初始化数据库import { Client } from 'pg'; import dotenv from 'dotenv'; dotenv.config(); //这里连接到数据库,如果连接失败就去检查你的.env文件 const client = new Client({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || 'student_management', user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'password', }); //这里开始初始化数据库 async function initDatabase() { try { console.log('连接数据库...'); await client.connect(); console.log('数据库连接成功'); // 创建学生表 console.log('创建学生表...'); await client.query(` CREATE TABLE IF NOT EXISTS students ( id SERIAL PRIMARY KEY, student_id VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, gender VARCHAR(10) CHECK (gender IN ('男', '女')), birth_date DATE, class_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), address TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); // 创建课程表 console.log('创建课程表...'); await client.query(` CREATE TABLE IF NOT EXISTS courses ( id SERIAL PRIMARY KEY, course_code VARCHAR(20) UNIQUE NOT NULL, course_name VARCHAR(100) NOT NULL, credits DECIMAL(3,1) NOT NULL, teacher_name VARCHAR(100), description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); // 创建成绩表 console.log('创建成绩表...'); await client.query(` CREATE TABLE IF NOT EXISTS grades ( id SERIAL PRIMARY KEY, student_id INTEGER REFERENCES students(id) ON DELETE CASCADE, course_id INTEGER REFERENCES courses(id) ON DELETE CASCADE, semester VARCHAR(20) NOT NULL, score DECIMAL(5,2) CHECK (score >= 0 AND score <= 100), grade VARCHAR(2) CHECK (grade IN ('A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'F')), exam_date DATE, remarks TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(student_id, course_id, semester) ); `); // 插入测试数据 console.log('插入测试数据...'); // 学生数据 实在想不出来什么好名字了。。。就张三李四吧,为啥都是高三呢因为作者高三了 await client.query(` INSERT INTO students (student_id, name, gender, birth_date, class_name, phone, email) VALUES ('2024001', '张三', '男', '2006-03-15', '高三(1)班', '13800138001', 'zhangsan@example.com'), ('2024002', '李四', '女', '2006-07-22', '高三(1)班', '13800138002', 'lisi@example.com'), ('2024003', '王五', '男', '2006-01-10', '高三(1)班', '13800138003', 'wangwu@example.com'), ON CONFLICT (student_id) DO NOTHING; `); // 课程数据 让高中生学高数是不是很好玩 await client.query(` INSERT INTO courses (course_code, course_name, credits, teacher_name, description) VALUES ('MATH101', '数学', 4.0, '陈老师', '高等数学基础课程'), ('CHI101', '语文', 4.0, '刘老师', '语文基础课程'), ('ENG101', '英语', 4.0, '王老师', '英语基础课程'), ('PHY101', '物理', 3.5, '李老师', '物理基础课程'), ('CHE101', '化学', 3.5, '张老师', '化学基础课程'), ('BIO101', '生物', 3.0, '赵老师', '生物基础课程') ON CONFLICT (course_code) DO NOTHING; `); // 成绩数据 await client.query(` INSERT INTO grades (student_id, course_id, semester, score, grade, exam_date) VALUES (1, 1, '2024-春季', 85.5, 'B+', '2024-06-15'), (1, 2, '2024-春季', 92.0, 'A', '2024-06-16'), (1, 3, '2024-春季', 78.5, 'C+', '2024-06-17'), (2, 1, '2024-春季', 95.0, 'A+', '2024-06-15'), (2, 2, '2024-春季', 88.5, 'B+', '2024-06-16'), (2, 3, '2024-春季', 91.0, 'A', '2024-06-17'), (3, 1, '2024-春季', 72.0, 'C', '2024-06-15'), (3, 2, '2024-春季', 85.0, 'B+', '2024-06-16'), (3, 3, '2024-春季', 79.5, 'C+', '2024-06-17') ON CONFLICT (student_id, course_id, semester) DO NOTHING; `); // 创建索引 console.log('创建索引...'); await client.query(` CREATE INDEX IF NOT EXISTS idx_students_student_id ON students(student_id); CREATE INDEX IF NOT EXISTS idx_students_class_name ON students(class_name); CREATE INDEX IF NOT EXISTS idx_courses_course_code ON courses(course_code); CREATE INDEX IF NOT EXISTS idx_grades_student_id ON grades(student_id); CREATE INDEX IF NOT EXISTS idx_grades_course_id ON grades(course_id); CREATE INDEX IF NOT EXISTS idx_grades_semester ON grades(semester); `); console.log('数据库初始化完成!'); // 显示统计 const stats = await client.query(` SELECT (SELECT COUNT(*) FROM students) as student_count, (SELECT COUNT(*) FROM courses) as course_count, (SELECT COUNT(*) FROM grades) as grade_count; `); console.log(`学生数量: ${stats.rows[0].student_count}`); console.log(`课程数量: ${stats.rows[0].course_count}`); console.log(`成绩记录: ${stats.rows[0].grade_count}`); } catch (error) { console.error('数据库初始化失败:', error); throw error; } finally { await client.end(); console.log('数据库连接已关闭'); } } if (require.main === module) { initDatabase() .then(() => { console.log('数据库初始化完成!'); process.exit(0); }) .catch((error) => { console.error('初始化失败:', error); process.exit(1); }); } export { initDatabase }; 创建 types/database.ts 类型文件// 学生信息 export interface Student { id: number; student_id: string; name: string; gender: '男' | '女'; birth_date: Date | null; class_name: string | null; phone: string | null; email: string | null; address: string | null; created_at: Date; updated_at: Date; } // 课程信息 export interface Course { id: number; course_code: string; course_name: string; credits: number; teacher_name: string | null; description: string | null; created_at: Date; updated_at: Date; } // 成绩信息 export interface Grade { id: number; student_id: number; course_id: number; semester: string; score: number | null; grade: 'A+' | 'A' | 'A-' | 'B+' | 'B' | 'B-' | 'C+' | 'C' | 'C-' | 'D+' | 'D' | 'F' | null; exam_date: Date | null; remarks: string | null; created_at: Date; updated_at: Date; } // 成绩详情(包含学生和课程信息) export interface GradeWithDetails { id: number; student_id: number; course_id: number; semester: string; score: number | null; grade: string | null; exam_date: Date | null; remarks: string | null; student_name: string; student_id_code: string; course_name: string; course_code: string; teacher_name: string | null; created_at: Date; updated_at: Date; } // 学生成绩统计 export interface StudentGradeStats { student_id: number; student_name: string; student_id_code: string; class_name: string | null; total_courses: number; average_score: number; total_credits: number; gpa: number; } // 课程成绩统计 export interface CourseGradeStats { course_id: number; course_name: string; course_code: string; teacher_name: string | null; total_students: number; average_score: number; highest_score: number; lowest_score: number; pass_rate: number; } // 查询参数 export interface StudentQueryParams { page?: number; limit?: number; search?: string; class_name?: string; gender?: '男' | '女'; } export interface GradeQueryParams { page?: number; limit?: number; student_id?: number; course_id?: number; semester?: string; min_score?: number; max_score?: number; } // 创建学生参数 export interface CreateStudentParams { student_id: string; name: string; gender: '男' | '女'; birth_date?: Date; class_name?: string; phone?: string; email?: string; address?: string; } // 更新学生参数 export interface UpdateStudentParams { name?: string; gender?: '男' | '女'; birth_date?: Date; class_name?: string; phone?: string; email?: string; address?: string; } // 创建成绩参数 export interface CreateGradeParams { student_id: number; course_id: number; semester: string; score?: number; grade?: string; exam_date?: Date; remarks?: string; } // 更新成绩参数 export interface UpdateGradeParams { score?: number; grade?: string; exam_date?: Date; remarks?: string; } // 创建课程参数 export interface CreateCourseParams { course_code: string; course_name: string; credits: number; teacher_name?: string; description?: string; } // 更新课程参数 export interface UpdateCourseParams { course_name?: string; credits?: number; teacher_name?: string; description?: string; } 创建lib/database.ts提供相关数据库操作方法import { Pool, PoolClient } from 'pg'; import dotenv from 'dotenv'; import { Student, Course, Grade, GradeWithDetails, StudentGradeStats, CourseGradeStats, StudentQueryParams, GradeQueryParams, CreateStudentParams, UpdateStudentParams, CreateGradeParams, UpdateGradeParams, CreateCourseParams, UpdateCourseParams, } from '../types/database'; dotenv.config(); class DatabaseService { private pool: Pool; constructor() { this.pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || 'student_management', user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'password', max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // 监听连接错误 this.pool.on('error', (err) => { console.error('数据库连接池错误:', err); }); } // 获取数据库连接 async getClient(): Promise<PoolClient> { return await this.pool.connect(); } // 关闭连接池 async close(): Promise<void> { await this.pool.end(); } // 学生相关方法 async getStudents(params: StudentQueryParams = {}): Promise<Student[]> { const { page = 1, limit = 10, search, class_name, gender } = params; const offset = (page - 1) * limit; let query = 'SELECT * FROM students WHERE 1=1'; const values: any[] = []; let paramIndex = 1; if (search) { query += ` AND (name ILIKE $${paramIndex} OR student_id ILIKE $${paramIndex})`; values.push(`%${search}%`); paramIndex++; } if (class_name) { query += ` AND class_name = $${paramIndex}`; values.push(class_name); paramIndex++; } if (gender) { query += ` AND gender = $${paramIndex}`; values.push(gender); paramIndex++; } query += ` ORDER BY created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; values.push(limit, offset); const client = await this.getClient(); try { const result = await client.query(query, values); return result.rows; } finally { client.release(); } } async getStudentById(id: number): Promise<Student | null> { const client = await this.getClient(); try { const result = await client.query('SELECT * FROM students WHERE id = $1', [id]); return result.rows[0] || null; } finally { client.release(); } } async getStudentByStudentId(studentId: string): Promise<Student | null> { const client = await this.getClient(); try { const result = await client.query('SELECT * FROM students WHERE student_id = $1', [studentId]); return result.rows[0] || null; } finally { client.release(); } } async createStudent(params: CreateStudentParams): Promise<Student> { const client = await this.getClient(); try { const result = await client.query(` INSERT INTO students (student_id, name, gender, birth_date, class_name, phone, email, address) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `, [ params.student_id, params.name, params.gender, params.birth_date, params.class_name, params.phone, params.email, params.address, ]); return result.rows[0]; } finally { client.release(); } } async updateStudent(id: number, params: UpdateStudentParams): Promise<Student | null> { const client = await this.getClient(); try { const fields = Object.keys(params).filter(key => params[key as keyof UpdateStudentParams] !== undefined); if (fields.length === 0) return null; const setClause = fields.map((field, index) => `${field} = $${index + 2}`).join(', '); const values = [id, ...fields.map(field => params[field as keyof UpdateStudentParams])]; const result = await client.query(` UPDATE students SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING * `, values); return result.rows[0] || null; } finally { client.release(); } } async deleteStudent(id: number): Promise<boolean> { const client = await this.getClient(); try { const result = await client.query('DELETE FROM students WHERE id = $1', [id]); return (result.rowCount || 0) > 0; } finally { client.release(); } } // 课程相关方法 async getCourses(): Promise<Course[]> { const client = await this.getClient(); try { const result = await client.query('SELECT * FROM courses ORDER BY course_code'); return result.rows; } finally { client.release(); } } async getCourseById(id: number): Promise<Course | null> { const client = await this.getClient(); try { const result = await client.query('SELECT * FROM courses WHERE id = $1', [id]); return result.rows[0] || null; } finally { client.release(); } } async createCourse(params: CreateCourseParams): Promise<Course> { const client = await this.getClient(); try { const result = await client.query(` INSERT INTO courses (course_code, course_name, credits, teacher_name, description) VALUES ($1, $2, $3, $4, $5) RETURNING * `, [ params.course_code, params.course_name, params.credits, params.teacher_name, params.description, ]); return result.rows[0]; } finally { client.release(); } } async updateCourse(id: number, params: UpdateCourseParams): Promise<Course | null> { const client = await this.getClient(); try { const fields = Object.keys(params).filter(key => params[key as keyof typeof params] !== undefined); if (fields.length === 0) return null; const setClause = fields.map((field, index) => `${field} = $${index + 2}`).join(', '); const values = [id, ...fields.map(field => params[field as keyof typeof params])]; const result = await client.query(` UPDATE courses SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING * `, values); return result.rows[0] || null; } finally { client.release(); } } // 成绩相关方法 async getGrades(params: GradeQueryParams = {}): Promise<GradeWithDetails[]> { const { page = 1, limit = 10, student_id, course_id, semester, min_score, max_score } = params; const offset = (page - 1) * limit; let query = ` SELECT g.*, s.name as student_name, s.student_id as student_id_code, c.course_name, c.course_code, c.teacher_name FROM grades g JOIN students s ON g.student_id = s.id JOIN courses c ON g.course_id = c.id WHERE 1=1 `; const values: any[] = []; let paramIndex = 1; if (student_id) { query += ` AND g.student_id = $${paramIndex}`; values.push(student_id); paramIndex++; } if (course_id) { query += ` AND g.course_id = $${paramIndex}`; values.push(course_id); paramIndex++; } if (semester) { query += ` AND g.semester = $${paramIndex}`; values.push(semester); paramIndex++; } if (min_score !== undefined) { query += ` AND g.score >= $${paramIndex}`; values.push(min_score); paramIndex++; } if (max_score !== undefined) { query += ` AND g.score <= $${paramIndex}`; values.push(max_score); paramIndex++; } query += ` ORDER BY g.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; values.push(limit, offset); const client = await this.getClient(); try { const result = await client.query(query, values); return result.rows; } finally { client.release(); } } async getGradeById(id: number): Promise<GradeWithDetails | null> { const client = await this.getClient(); try { const result = await client.query(` SELECT g.*, s.name as student_name, s.student_id as student_id_code, c.course_name, c.course_code, c.teacher_name FROM grades g JOIN students s ON g.student_id = s.id JOIN courses c ON g.course_id = c.id WHERE g.id = $1 `, [id]); return result.rows[0] || null; } finally { client.release(); } } async createGrade(params: CreateGradeParams): Promise<Grade> { const client = await this.getClient(); try { const result = await client.query(` INSERT INTO grades (student_id, course_id, semester, score, grade, exam_date, remarks) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `, [ params.student_id, params.course_id, params.semester, params.score, params.grade, params.exam_date, params.remarks, ]); return result.rows[0]; } finally { client.release(); } } async updateGrade(id: number, params: UpdateGradeParams): Promise<Grade | null> { const client = await this.getClient(); try { const fields = Object.keys(params).filter(key => params[key as keyof UpdateGradeParams] !== undefined); if (fields.length === 0) return null; const setClause = fields.map((field, index) => `${field} = $${index + 2}`).join(', '); const values = [id, ...fields.map(field => params[field as keyof UpdateGradeParams])]; const result = await client.query(` UPDATE grades SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING * `, values); return result.rows[0] || null; } finally { client.release(); } } async deleteGrade(id: number): Promise<boolean> { const client = await this.getClient(); try { const result = await client.query('DELETE FROM grades WHERE id = $1', [id]); return (result.rowCount || 0) > 0; } finally { client.release(); } } // 统计方法 async getStudentGradeStats(studentId: number): Promise<StudentGradeStats | null> { const client = await this.getClient(); try { const result = await client.query(` SELECT s.id as student_id, s.name as student_name, s.student_id as student_id_code, s.class_name, COUNT(g.id) as total_courses, AVG(g.score) as average_score, SUM(c.credits) as total_credits, AVG(CASE WHEN g.grade = 'A+' THEN 4.3 WHEN g.grade = 'A' THEN 4.0 WHEN g.grade = 'A-' THEN 3.7 WHEN g.grade = 'B+' THEN 3.3 WHEN g.grade = 'B' THEN 3.0 WHEN g.grade = 'B-' THEN 2.7 WHEN g.grade = 'C+' THEN 2.3 WHEN g.grade = 'C' THEN 2.0 WHEN g.grade = 'C-' THEN 1.7 WHEN g.grade = 'D+' THEN 1.3 WHEN g.grade = 'D' THEN 1.0 WHEN g.grade = 'F' THEN 0.0 ELSE NULL END) as gpa FROM students s LEFT JOIN grades g ON s.id = g.student_id LEFT JOIN courses c ON g.course_id = c.id WHERE s.id = $1 GROUP BY s.id, s.name, s.student_id, s.class_name `, [studentId]); return result.rows[0] || null; } finally { client.release(); } } async getCourseGradeStats(courseId: number): Promise<CourseGradeStats | null> { const client = await this.getClient(); try { const result = await client.query(` SELECT c.id as course_id, c.course_name, c.course_code, c.teacher_name, COUNT(g.id) as total_students, AVG(g.score) as average_score, MAX(g.score) as highest_score, MIN(g.score) as lowest_score, (COUNT(CASE WHEN g.score >= 60 THEN 1 END) * 100.0 / COUNT(g.id)) as pass_rate FROM courses c LEFT JOIN grades g ON c.id = g.course_id WHERE c.id = $1 GROUP BY c.id, c.course_name, c.course_code, c.teacher_name `, [courseId]); return result.rows[0] || null; } finally { client.release(); } } } // 创建单例实例 const databaseService = new DatabaseService(); export default databaseService; 步骤4:初始化数据库在package.json的scripts中添加 "db:init": "tsx scripts/init-database.ts", 然后执行bun run db:init看到输出数据库初始化完成!即为成功初始化初始化后可以在华为云的数据库管理界面看到有三张表被创建了!3.6 编写业务代码步骤1:trpc路由创建app/lib/routers/course.tsimport { z } from 'zod'; import { createTRPCRouter, publicProcedure } from '../trpc'; import databaseService from '../../../lib/database'; export const courseRouter = createTRPCRouter({ // 获取所有课程 getAll: publicProcedure .query(async () => { return await databaseService.getCourses(); }), // 根据ID获取课程 getById: publicProcedure .input(z.number()) .query(async ({ input }) => { return await databaseService.getCourseById(input); }), // 创建课程 create: publicProcedure .input(z.object({ course_code: z.string(), course_name: z.string(), credits: z.number(), teacher_name: z.string().optional(), description: z.string().optional(), })) .mutation(async ({ input }) => { return await databaseService.createCourse(input); }), // 更新课程 update: publicProcedure .input(z.object({ id: z.number(), data: z.object({ course_name: z.string().optional(), credits: z.number().optional(), teacher_name: z.string().optional(), description: z.string().optional(), }), })) .mutation(async ({ input }) => { return await databaseService.updateCourse(input.id, input.data); }), // 获取课程成绩统计 getGradeStats: publicProcedure .input(z.number()) .query(async ({ input }) => { return await databaseService.getCourseGradeStats(input); }), }); 创建app/lib/routers/grade.tsimport { z } from 'zod'; import { createTRPCRouter, publicProcedure } from '../trpc'; import databaseService from '../../../lib/database'; export const gradeRouter = createTRPCRouter({ // 获取所有成绩 getAll: publicProcedure .input(z.object({ page: z.number().optional(), limit: z.number().optional(), student_id: z.number().optional(), course_id: z.number().optional(), semester: z.string().optional(), min_score: z.number().optional(), max_score: z.number().optional(), }).optional()) .query(async ({ input }) => { return await databaseService.getGrades(input); }), // 根据ID获取成绩 getById: publicProcedure .input(z.number()) .query(async ({ input }) => { return await databaseService.getGradeById(input); }), // 创建成绩 create: publicProcedure .input(z.object({ student_id: z.number(), course_id: z.number(), semester: z.string(), score: z.number().optional(), grade: z.string().optional(), exam_date: z.date().optional(), remarks: z.string().optional(), })) .mutation(async ({ input }) => { return await databaseService.createGrade(input); }), // 更新成绩 update: publicProcedure .input(z.object({ id: z.number(), data: z.object({ score: z.number().optional(), grade: z.string().optional(), exam_date: z.date().optional(), remarks: z.string().optional(), }), })) .mutation(async ({ input }) => { return await databaseService.updateGrade(input.id, input.data); }), // 删除成绩 delete: publicProcedure .input(z.number()) .mutation(async ({ input }) => { return await databaseService.deleteGrade(input); }), // 获取学生成绩 getByStudent: publicProcedure .input(z.object({ student_id: z.number(), semester: z.string().optional(), })) .query(async ({ input }) => { return await databaseService.getGrades({ student_id: input.student_id, semester: input.semester, }); }), // 获取课程成绩 getByCourse: publicProcedure .input(z.object({ course_id: z.number(), semester: z.string().optional(), })) .query(async ({ input }) => { return await databaseService.getGrades({ course_id: input.course_id, semester: input.semester, }); }), }); 创建app/lib/routers/student.tsimport { z } from 'zod'; import { createTRPCRouter, publicProcedure } from '../trpc'; import databaseService from '../../../lib/database'; export const studentRouter = createTRPCRouter({ // 获取所有学生 getAll: publicProcedure .input(z.object({ page: z.number().optional(), limit: z.number().optional(), search: z.string().optional(), class_name: z.string().optional(), gender: z.enum(['男', '女']).optional(), }).optional()) .query(async ({ input }) => { return await databaseService.getStudents(input); }), // 根据ID获取学生 getById: publicProcedure .input(z.number()) .query(async ({ input }) => { return await databaseService.getStudentById(input); }), // 根据学号获取学生 getByStudentId: publicProcedure .input(z.string()) .query(async ({ input }) => { return await databaseService.getStudentByStudentId(input); }), // 创建学生 create: publicProcedure .input(z.object({ student_id: z.string(), name: z.string(), gender: z.enum(['男', '女']), birth_date: z.date().optional(), class_name: z.string().optional(), phone: z.string().optional(), email: z.string().email().optional(), address: z.string().optional(), })) .mutation(async ({ input }) => { return await databaseService.createStudent(input); }), // 更新学生 update: publicProcedure .input(z.object({ id: z.number(), data: z.object({ name: z.string().optional(), gender: z.enum(['男', '女']).optional(), birth_date: z.date().optional(), class_name: z.string().optional(), phone: z.string().optional(), email: z.string().email().optional(), address: z.string().optional(), }), })) .mutation(async ({ input }) => { return await databaseService.updateStudent(input.id, input.data); }), // 删除学生 delete: publicProcedure .input(z.number()) .mutation(async ({ input }) => { return await databaseService.deleteStudent(input); }), // 获取学生成绩统计 getGradeStats: publicProcedure .input(z.number()) .query(async ({ input }) => { return await databaseService.getStudentGradeStats(input); }), }); 更新主路由文件app/lib/routers/_app.tsimport { createTRPCRouter } from '../trpc'; import { studentRouter } from './student'; import { courseRouter } from './course'; import { gradeRouter } from './grade'; export const appRouter = createTRPCRouter({ student: studentRouter, course: courseRouter, grade: gradeRouter, }); export type AppRouter = typeof appRouter; 步骤2:编写前端页面app/page.tsx'use client'; import { useState } from 'react'; import { trpc } from "../utils/trpc"; import Navigation from './components/Navigation'; import StudentManager from './components/StudentManager'; import CourseManager from './components/CourseManager'; import GradeManager from './components/GradeManager'; import ReportManager from './components/ReportManager'; function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) { return ( <div className="bg-white rounded-lg shadow p-6"> <div className="flex items-center"> <div className="flex-1"> <p className="text-sm text-black">{title}</p> <p className="text-2xl font-bold text-black">{value}</p> </div> <div className="text-3xl">{icon}</div> </div> </div> ); } function SimpleTable({ title, data, columns }: { title: string; data: any[]; columns: { key: string; label: string }[]; }) { if (!data || data.length === 0) { return ( <div className="bg-white rounded-lg shadow p-6"> <h3 className="text-lg font-semibold mb-4 text-black">{title}</h3> <p className="text-black">暂无数据</p> </div> ); } return ( <div className="rounded-lg shadow p-6 bg-white"> <h3 className="text-lg font-semibold mb-4 text-black">{title}</h3> <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b"> {columns.map((col) => ( <th key={col.key} className="text-left py-2 px-4 font-medium text-black"> {col.label} </th> ))} </tr> </thead> <tbody> {data.slice(0, 5).map((row, index) => ( <tr key={index} className="border-b hover:bg-gray-50 text-black"> {columns.map((col) => ( <td key={col.key} className="py-2 px-4 text-black"> {row[col.key] || '-'} </td> ))} </tr> ))} </tbody> </table> </div> {data.length > 5 && ( <p className="text-sm text-gray-500 mt-2">显示前5条,共{data.length}条</p> )} </div> ); } export default function HomePage() { const [currentPage, setCurrentPage] = useState('dashboard'); const students = trpc.student.getAll.useQuery(); const courses = trpc.course.getAll.useQuery(); const grades = trpc.grade.getAll.useQuery({ limit: 10 }); const allGrades = trpc.grade.getAll.useQuery(); const totalStudents = students.data?.length || 0; const totalCourses = courses.data?.length || 0; const totalGrades = allGrades.data?.length || 0; const validGrades = allGrades.data?.filter(grade => grade.score !== null && grade.score !== undefined) || []; const avgScore = validGrades.length > 0 ? (validGrades.reduce((sum, grade) => sum + (parseFloat(String(grade.score)) || 0), 0) / validGrades.length).toFixed(1) : '0.0'; const showPage = () => { switch (currentPage) { case 'students': return <StudentManager />; case 'courses': return <CourseManager />; case 'grades': return <GradeManager />; case 'reports': return <ReportManager />; default: return ( <div className="space-y-6"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <StatCard title="学生总数" value={totalStudents} icon="👥" /> <StatCard title="课程总数" value={totalCourses} icon="📚" /> <StatCard title="成绩记录" value={totalGrades} icon="📊" /> <StatCard title="平均分" value={avgScore} icon="⭐" /> </div> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <SimpleTable title="学生列表" data={students.data || []} columns={[ { key: 'name', label: '姓名' }, { key: 'student_id', label: '学号' }, { key: 'class_name', label: '班级' } ]} /> <SimpleTable title="课程列表" data={courses.data || []} columns={[ { key: 'course_name', label: '课程名称' }, { key: 'teacher_name', label: '教师' }, { key: 'credits', label: '学分' } ]} /> </div> <SimpleTable title="最新成绩" data={grades.data || []} columns={[ { key: 'student_name', label: '学生' }, { key: 'course_name', label: '课程' }, { key: 'score', label: '分数' }, { key: 'semester', label: '学期' } ]} /> </div> ); } }; return ( <div className="min-h-screen bg-gray-50"> <Navigation currentPage={currentPage} onPageChange={setCurrentPage} /> <main className="max-w-7xl mx-auto px-4 py-8"> {showPage()} </main> <footer className="bg-white border-t mt-12 py-6"> <div className="max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm"> <p>学生成绩管理系统 作者dylan su</p> </div> </footer> </div> ); } app/components/Navigation.tsx'use client'; import { useState } from 'react'; interface NavigationProps { currentPage: string; onPageChange: (page: string) => void; } export default function Navigation({ currentPage, onPageChange }: NavigationProps) { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const menuItems = [ { id: 'dashboard', label: '仪表板' }, { id: 'students', label: '学生管理' }, { id: 'courses', label: '课程管理'}, { id: 'grades', label: '成绩管理' }, { id: 'reports', label: '统计报表'}, ]; return ( <nav className="bg-white shadow-sm border-b"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16"> <div className="flex items-center"> <div className="flex-shrink-0 flex items-center"> <span className="text-xl font-bold text-gray-900">学生管理系统</span> </div> </div> <div className="hidden md:flex items-center space-x-8"> {menuItems.map((item) => ( <button key={item.id} onClick={() => onPageChange(item.id)} className={`flex items-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${ currentPage === item.id ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50' }`} > {item.label} </button> ))} </div> <div className="md:hidden flex items-center"> <button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" > <span className="sr-only">打开主菜单</span> {isMobileMenuOpen ? ( <svg className="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> ) : ( <svg className="block h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> </svg> )} </button> </div> </div> </div> {isMobileMenuOpen && ( <div className="md:hidden"> <div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-white border-t"> {menuItems.map((item) => ( <button key={item.id} onClick={() => { onPageChange(item.id); setIsMobileMenuOpen(false); }} className={`flex items-center w-full px-3 py-2 rounded-md text-base font-medium transition-colors ${ currentPage === item.id ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50' }`} > {item.label} </button> ))} </div> </div> )} </nav> ); } app/components/StudentManager.tsx'use client'; import { useState } from 'react'; import { trpc } from '../../utils/trpc'; export default function StudentManager() { const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState<number | null>(null); const [form, setForm] = useState({ student_id: '', name: '', gender: '男' as '男' | '女', class_name: '', phone: '', email: '' }); const students = trpc.student.getAll.useQuery(); const createStudent = trpc.student.create.useMutation({ onSuccess: () => { students.refetch(); resetForm(); } }); const updateStudent = trpc.student.update.useMutation({ onSuccess: () => { students.refetch(); resetForm(); } }); const deleteStudent = trpc.student.delete.useMutation({ onSuccess: () => students.refetch() }); const resetForm = () => { setForm({ student_id: '', name: '', gender: '男', class_name: '', phone: '', email: '' }); setEditingId(null); setShowForm(false); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (editingId) { updateStudent.mutate({ id: editingId, data: form }); } else { createStudent.mutate(form); } }; const handleEdit = (student: any) => { setEditingId(student.id); setForm({ student_id: student.student_id, name: student.name, gender: student.gender, class_name: student.class_name || '', phone: student.phone || '', email: student.email || '' }); setShowForm(true); }; const handleDelete = (id: number) => { if (confirm('确定删除这个学生?')) { deleteStudent.mutate(id); } }; return ( <div className="space-y-6 text-black"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">学生管理</h1> <button onClick={() => setShowForm(true)} className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" > 添加学生 </button> </div> <div className="bg-white rounded-lg shadow p-6"> <h2 className="text-lg font-semibold mb-4">学生列表</h2> {students.isLoading ? ( <p>加载中...</p> ) : students.error ? ( <p className="text-red-500">加载失败</p> ) : ( <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b"> <th className="text-left py-2 px-4">学号</th> <th className="text-left py-2 px-4">姓名</th> <th className="text-left py-2 px-4">性别</th> <th className="text-left py-2 px-4">班级</th> <th className="text-left py-2 px-4">操作</th> </tr> </thead> <tbody> {students.data?.map((student) => ( <tr key={student.id} className="border-b hover:bg-gray-50"> <td className="py-2 px-4">{student.student_id}</td> <td className="py-2 px-4">{student.name}</td> <td className="py-2 px-4">{student.gender}</td> <td className="py-2 px-4">{student.class_name || '-'}</td> <td className="py-2 px-4"> <button onClick={() => handleEdit(student)} className="text-blue-600 hover:text-blue-800 mr-2" > 编辑 </button> <button onClick={() => handleDelete(student.id)} className="text-red-600 hover:text-red-800" > 删除 </button> </td> </tr> ))} </tbody> </table> </div> )} </div> {showForm && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"> <div className="bg-white rounded-lg p-6 w-full max-w-md"> <h2 className="text-lg font-semibold mb-4"> {editingId ? '编辑学生' : '添加学生'} </h2> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">学号</label> <input type="text" value={form.student_id} onChange={(e) => setForm({...form, student_id: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">姓名</label> <input type="text" value={form.name} onChange={(e) => setForm({...form, name: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">性别</label> <select value={form.gender} onChange={(e) => setForm({...form, gender: e.target.value as '男' | '女'})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="男">男</option> <option value="女">女</option> </select> </div> <div> <label className="block text-sm font-medium mb-1">班级</label> <input type="text" value={form.class_name} onChange={(e) => setForm({...form, class_name: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium mb-1">电话</label> <input type="text" value={form.phone} onChange={(e) => setForm({...form, phone: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium mb-1">邮箱</label> <input type="email" value={form.email} onChange={(e) => setForm({...form, email: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div className="flex gap-2 pt-4"> <button type="submit" className="flex-1 bg-blue-500 text-white py-2 rounded hover:bg-blue-600" disabled={createStudent.isPending || updateStudent.isPending} > {createStudent.isPending || updateStudent.isPending ? '保存中...' : '保存'} </button> <button type="button" onClick={resetForm} className="flex-1 bg-gray-500 text-white py-2 rounded hover:bg-gray-600" > 取消 </button> </div> </form> </div> </div> )} </div> ); } app/components/ReportManager.tsx'use client'; import { trpc } from '../../utils/trpc'; export default function ReportManager() { const students = trpc.student.getAll.useQuery(); const courses = trpc.course.getAll.useQuery(); const grades = trpc.grade.getAll.useQuery(); // 计算统计数据 const totalStudents = students.data?.length || 0; const totalCourses = courses.data?.length || 0; const totalGrades = grades.data?.length || 0; // 计算平均分 - 只计算有效的成绩,处理字符串类型的分数 const validGrades = grades.data?.filter(grade => grade.score !== null && grade.score !== undefined) || []; const avgScore = validGrades.length > 0 ? (validGrades.reduce((sum, grade) => sum + (parseFloat(String(grade.score)) || 0), 0) / validGrades.length).toFixed(1) : '0.0'; // 分数分布 - 只使用有效的成绩,处理字符串类型的分数 const scoreDistribution = validGrades.length > 0 ? { excellent: validGrades.filter(g => parseFloat(String(g.score)) >= 90).length, good: validGrades.filter(g => parseFloat(String(g.score)) >= 80 && parseFloat(String(g.score)) < 90).length, average: validGrades.filter(g => parseFloat(String(g.score)) >= 70 && parseFloat(String(g.score)) < 80).length, poor: validGrades.filter(g => parseFloat(String(g.score)) < 70).length } : { excellent: 0, good: 0, average: 0, poor: 0 }; return ( <div className="space-y-6 text-black"> <h1 className="text-2xl font-bold">统计报表</h1> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="bg-white rounded-lg shadow p-6"> <div className="flex items-center"> <div className="flex-1"> <p className="text-sm text-black">学生总数</p> <p className="text-2xl font-bold text-black">{totalStudents}</p> </div> <div className="text-3xl">👥</div> </div> </div> <div className="bg-white rounded-lg shadow p-6"> <div className="flex items-center"> <div className="flex-1"> <p className="text-sm text-black">课程总数</p> <p className="text-2xl font-bold text-black">{totalCourses}</p> </div> <div className="text-3xl">📚</div> </div> </div> <div className="bg-white rounded-lg shadow p-6"> <div className="flex items-center"> <div className="flex-1"> <p className="text-sm text-black">成绩记录</p> <p className="text-2xl font-bold text-black">{totalGrades}</p> </div> <div className="text-3xl">📊</div> </div> </div> <div className="bg-white rounded-lg shadow p-6"> <div className="flex items-center"> <div className="flex-1"> <p className="text-sm text-black">平均分</p> <p className="text-2xl font-bold text-black">{avgScore}</p> </div> <div className="text-3xl">⭐</div> </div> </div> </div> <div className="bg-white rounded-lg shadow p-6"> <h2 className="text-lg font-semibold mb-4">分数分布</h2> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="text-center"> <div className="text-2xl font-bold text-green-600">{scoreDistribution.excellent}</div> <div className="text-sm text-gray-600">优秀 (90+)</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-blue-600">{scoreDistribution.good}</div> <div className="text-sm text-gray-600">良好 (80-89)</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-yellow-600">{scoreDistribution.average}</div> <div className="text-sm text-gray-600">中等 (70-79)</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-red-600">{scoreDistribution.poor}</div> <div className="text-sm text-gray-600">需努力 (&lt;70)</div> </div> </div> </div> <div className="bg-white rounded-lg shadow p-6"> <h2 className="text-lg font-semibold mb-4">课程平均分</h2> {courses.isLoading ? ( <p>加载中...</p> ) : ( <div className="space-y-3"> {courses.data?.map((course) => { const courseGrades = grades.data?.filter(g => g.course_id === course.id) || []; const avg = courseGrades.length > 0 ? (courseGrades.reduce((sum, g) => sum + (parseFloat(String(g.score)) || 0), 0) / courseGrades.length).toFixed(1) : '0.0'; return ( <div key={course.id} className="flex justify-between items-center py-2 border-b"> <span>{course.course_name}</span> <span className="font-semibold">{avg}分</span> </div> ); })} </div> )} </div> </div> ); } app/components/GradeManager.tsx'use client'; import { useState } from 'react'; import { trpc } from '../../utils/trpc'; export default function GradeManager() { const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState<number | null>(null); const [form, setForm] = useState({ student_id: 0, course_id: 0, score: 0, semester: '' }); const grades = trpc.grade.getAll.useQuery(); const students = trpc.student.getAll.useQuery(); const courses = trpc.course.getAll.useQuery(); const createGrade = trpc.grade.create.useMutation({ onSuccess: () => { grades.refetch(); resetForm(); } }); const updateGrade = trpc.grade.update.useMutation({ onSuccess: () => { grades.refetch(); resetForm(); } }); const deleteGrade = trpc.grade.delete.useMutation({ onSuccess: () => grades.refetch() }); const resetForm = () => { setForm({ student_id: 0, course_id: 0, score: 0, semester: '' }); setEditingId(null); setShowForm(false); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (editingId) { updateGrade.mutate({ id: editingId, data: form }); } else { createGrade.mutate(form); } }; const handleEdit = (grade: any) => { setEditingId(grade.id); setForm({ student_id: grade.student_id, course_id: grade.course_id, score: grade.score, semester: grade.semester }); setShowForm(true); }; const handleDelete = (id: number) => { if (confirm('确定删除这个成绩?')) { deleteGrade.mutate(id); } }; const getGradeLevel = (score: number) => { if (score >= 90) return { text: '优秀', color: 'text-green-600' }; if (score >= 80) return { text: '良好', color: 'text-blue-600' }; if (score >= 70) return { text: '中等', color: 'text-yellow-600' }; return { text: '需努力', color: 'text-red-600' }; }; return ( <div className="space-y-6 text-black"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">成绩管理</h1> <button onClick={() => setShowForm(true)} className="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600" > 录入成绩 </button> </div> <div className="bg-white rounded-lg shadow p-6"> <h2 className="text-lg font-semibold mb-4">成绩列表</h2> {grades.isLoading ? ( <p>加载中...</p> ) : grades.error ? ( <p className="text-red-500">加载失败</p> ) : ( <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b"> <th className="text-left py-2 px-4">学生</th> <th className="text-left py-2 px-4">课程</th> <th className="text-left py-2 px-4">分数</th> <th className="text-left py-2 px-4">等级</th> <th className="text-left py-2 px-4">学期</th> <th className="text-left py-2 px-4">操作</th> </tr> </thead> <tbody> {grades.data?.map((grade) => { const level = getGradeLevel(grade.score || 0); return ( <tr key={grade.id} className="border-b hover:bg-gray-50"> <td className="py-2 px-4">{grade.student_name}</td> <td className="py-2 px-4">{grade.course_name}</td> <td className="py-2 px-4">{grade.score}</td> <td className={`py-2 px-4 ${level.color}`}>{level.text}</td> <td className="py-2 px-4">{grade.semester}</td> <td className="py-2 px-4"> <button onClick={() => handleEdit(grade)} className="text-blue-600 hover:text-blue-800 mr-2" > 编辑 </button> <button onClick={() => handleDelete(grade.id)} className="text-red-600 hover:text-red-800" > 删除 </button> </td> </tr> ); })} </tbody> </table> </div> )} </div> {showForm && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"> <div className="bg-white rounded-lg p-6 w-full max-w-md"> <h2 className="text-lg font-semibold mb-4"> {editingId ? '编辑成绩' : '录入成绩'} </h2> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">学生</label> <select value={form.student_id} onChange={(e) => setForm({...form, student_id: parseInt(e.target.value)})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required > <option value="">选择学生</option> {students.data?.map((student) => ( <option key={student.id} value={student.id}> {student.name} ({student.student_id}) </option> ))} </select> </div> <div> <label className="block text-sm font-medium mb-1">课程</label> <select value={form.course_id} onChange={(e) => setForm({...form, course_id: parseInt(e.target.value)})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required > <option value="">选择课程</option> {courses.data?.map((course) => ( <option key={course.id} value={course.id}> {course.course_name} ({course.course_code}) </option> ))} </select> </div> <div> <label className="block text-sm font-medium mb-1">分数</label> <input type="number" min="0" max="100" value={form.score} onChange={(e) => setForm({...form, score: parseInt(e.target.value)})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">学期</label> <input type="text" value={form.semester} onChange={(e) => setForm({...form, semester: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="如:2024春季" required /> </div> <div className="flex gap-2 pt-4"> <button type="submit" className="flex-1 bg-purple-500 text-white py-2 rounded hover:bg-purple-600" disabled={createGrade.isPending || updateGrade.isPending} > {createGrade.isPending || updateGrade.isPending ? '保存中...' : '保存'} </button> <button type="button" onClick={resetForm} className="flex-1 bg-gray-500 text-white py-2 rounded hover:bg-gray-600" > 取消 </button> </div> </form> </div> </div> )} </div> ); } app/components/CourseManager.tsx'use client'; import { useState } from 'react'; import { trpc } from '../../utils/trpc'; export default function CourseManager() { const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState<number | null>(null); const [form, setForm] = useState({ course_code: '', course_name: '', credits: 0, teacher_name: '', description: '' }); const courses = trpc.course.getAll.useQuery(); const createCourse = trpc.course.create.useMutation({ onSuccess: () => { courses.refetch(); resetForm(); } }); const updateCourse = trpc.course.update.useMutation({ onSuccess: () => { courses.refetch(); resetForm(); } }); const resetForm = () => { setForm({ course_code: '', course_name: '', credits: 0, teacher_name: '', description: '' }); setEditingId(null); setShowForm(false); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (editingId) { updateCourse.mutate({ id: editingId, data: form }); } else { createCourse.mutate(form); } }; const handleEdit = (course: any) => { setEditingId(course.id); setForm({ course_code: course.course_code, course_name: course.course_name, credits: course.credits, teacher_name: course.teacher_name || '', description: course.description || '' }); setShowForm(true); }; return ( <div className="space-y-6 text-black"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">课程管理</h1> <button onClick={() => setShowForm(true)} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" > 添加课程 </button> </div> <div className="bg-white rounded-lg shadow p-6"> <h2 className="text-lg font-semibold mb-4">课程列表</h2> {courses.isLoading ? ( <p>加载中...</p> ) : courses.error ? ( <p className="text-red-500">加载失败</p> ) : ( <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b"> <th className="text-left py-2 px-4">课程代码</th> <th className="text-left py-2 px-4">课程名称</th> <th className="text-left py-2 px-4">学分</th> <th className="text-left py-2 px-4">教师</th> <th className="text-left py-2 px-4">操作</th> </tr> </thead> <tbody> {courses.data?.map((course) => ( <tr key={course.id} className="border-b hover:bg-gray-50"> <td className="py-2 px-4">{course.course_code}</td> <td className="py-2 px-4">{course.course_name}</td> <td className="py-2 px-4">{course.credits}</td> <td className="py-2 px-4">{course.teacher_name || '-'}</td> <td className="py-2 px-4"> <button onClick={() => handleEdit(course)} className="text-blue-600 hover:text-blue-800" > 编辑 </button> </td> </tr> ))} </tbody> </table> </div> )} </div> {showForm && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"> <div className="bg-white rounded-lg p-6 w-full max-w-md"> <h2 className="text-lg font-semibold mb-4"> {editingId ? '编辑课程' : '添加课程'} </h2> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label className="block text-sm font-medium mb-1">课程代码</label> <input type="text" value={form.course_code} onChange={(e) => setForm({...form, course_code: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">课程名称</label> <input type="text" value={form.course_name} onChange={(e) => setForm({...form, course_name: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">学分</label> <input type="number" value={form.credits} onChange={(e) => setForm({...form, credits: parseInt(e.target.value)})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" required /> </div> <div> <label className="block text-sm font-medium mb-1">教师</label> <input type="text" value={form.teacher_name} onChange={(e) => setForm({...form, teacher_name: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium mb-1">描述</label> <textarea value={form.description} onChange={(e) => setForm({...form, description: e.target.value})} className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" rows={3} /> </div> <div className="flex gap-2 pt-4"> <button type="submit" className="flex-1 bg-green-500 text-white py-2 rounded hover:bg-green-600" disabled={createCourse.isPending || updateCourse.isPending} > {createCourse.isPending || updateCourse.isPending ? '保存中...' : '保存'} </button> <button type="button" onClick={resetForm} className="flex-1 bg-gray-500 text-white py-2 rounded hover:bg-gray-600" > 取消 </button> </div> </form> </div> </div> )} </div> ); } 3.6 运行项目执行bun dev然后看到终端返回打开http://localhost:3000即可看到项目运行的状况4.1 页面展示4.1.1仪表盘这是学生管理系统的仪表板页面,作为系统的主控制面板,它通过四个关键数据卡片(学生总数6人、课程总数10门、成绩记录10条、平均分77.0)快速展示系统整体状况,同时以表格形式分别列出了学生基本信息、课程设置和最新成绩记录,让管理员能够一目了然地掌握当前教学管理的核心数据和最新动态。4.1.2 学生管理这是学生管理系统的学生管理页面,专门用于学生信息的维护和管理。页面顶部设有"添加学生"按钮用于新增学生记录,主体部分以表格形式展示了6名学生的详细信息,包括学号(2024001-2024006)、姓名、性别和所属班级,每行末尾都提供"编辑"和"删除"操作按钮,方便管理员对学生信息进行实时的增删改查操作。4.1.3 课程管理这是学生管理系统的课程管理页面,用于管理学校的课程信息。页面顶部提供"添加课程"按钮用于新增课程,主体部分以表格形式展示了10门课程的详细信息,包括:核心课程:数学、语文、英语、物理、化学、生物等主要学科每门课程都配有相应的任课教师和学分设置课程信息包含:课程代码(如MATH101、CHI101等标准化编码)课程名称学分(2.0-4.0学分不等)任课教师编辑操作按钮4.1.4 成绩管理这是学生管理系统的成绩管理页面,用于记录和管理学生的考试成绩。页面功能包括:主要功能录入成绩:页面右上角提供"录入成绩"按钮,用于添加新的成绩记录成绩查看:以表格形式展示所有学生的成绩信息成绩信息展示页面显示了3名学生(张三、李四、王五)在2024春季学期的各科成绩:学生成绩概况:张三:语文92分(优秀)、英语78.5分(中等)、数学85.5分(良好)李四:数学95分(优秀)、语文88.5分(良好)、英语91分(优秀)王五:数学72分(中等)、语文85分(良好)、英语79.5分(中等)每条成绩记录都提供"编辑"和"删除"操作,方便教师对成绩进行修改和维护。整个界面设计清晰,便于教务人员进行成绩管理工作。4.1.5 统计报表5.1总结本项目基于华为开发者空间的云开发环境,采用 Bun + Next.js + tRPC 现代化全栈技术栈,成功构建了一个功能完善的学生成绩管理系统。系统包含课程管理和成绩管理两大核心模块,实现了标准化的课程编码体系和科学的成绩评价机制。通过 Bun 超快运行时提升构建效率,Next.js 处理前后端复杂逻辑,tRPC 确保端到端类型安全,原生 SQL 直接对接华为 GaussDB 数据库实现最优查询性能。项目充分利用华为开发者空间提供的免费数据库实例和完整开发工具链,体验了从开发编码到应用调测的全流程云开发模式。系统界面简洁直观,支持课程信息维护、成绩录入编辑、智能等级评定等功能,为学校教务管理提供了高效便捷的数字化解决方案。通过实际开发实践,深入了解了现代全栈开发最佳实践和华为根技术生态的强大能力,为未来进一步集成昇腾AI、鸿蒙等技术打下了坚实基础。至此本案例关于在开发者空间–远程开发环境中部署Bun + Next.js + tRPC与开发者空间生态版GaussDB实践操作并编写学生成绩管理系统完毕。我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用(https://bbs.huaweicloud.com/forum/thread-0229189398343651003-1-1.html)
  • PostgreSQL语法解析:从基础到进阶的完整指南
    PostgreSQL语法解析:从基础到进阶的完整指南PostgreSQL作为全球最先进的开源关系型数据库之一,其语法体系既遵循SQL标准又具备独特扩展性。本文将从语法解析的底层原理出发,结合实际开发场景,深入剖析PostgreSQL的语法特性与优化实践。一、语法解析引擎架构PostgreSQL采用经典的LALR(1)语法分析器,其核心解析流程分为三个阶段:词法分析:通过Flex生成的scan.l文件将SQL语句拆解为Token流语法分析:Bison生成的gram.y文件定义了完整的SQL语法规则树语义分析:在parser.c中完成类型检查、权限验证等逻辑以SELECT * FROM users WHERE id=1为例,解析过程会生成如下语法树:SelectStmt ├── targetList: [*] ├── fromClause: [users] └── whereClause: [OpExpr(=, id, 1)] 二、核心语法特性深度解析1. 表表达式处理PostgreSQL的表表达式支持五种高级操作:WITH子句(CTE):WITH regional_sales AS ( SELECT region, SUM(amount) AS total_sales FROM orders GROUP BY region ) SELECT region, total_sales FROM regional_sales WHERE total_sales > (SELECT AVG(total_sales) FROM regional_sales); LATERAL连接:SELECT d.dept_name, e.emp_name FROM departments d, LATERAL ( SELECT emp_name FROM employees WHERE dept_id = d.id ORDER BY hire_date DESC LIMIT 1 ) e; 窗口函数:SELECT emp_id, salary, RANK() OVER (PARTITION BY dept ORDER BY salary DESC) as dept_rank, PERCENT_RANK() OVER (ORDER BY salary) as company_percentile FROM employees; 2. 数据类型系统PostgreSQL支持90+种内置数据类型,其中特色类型包括:JSONB:二进制存储的JSON,支持GIN索引-- 创建JSONB索引 CREATE INDEX idx_products_attrs ON products USING gin (attributes); -- JSON路径查询 SELECT name FROM products WHERE attributes @> '{"color": "red"}'; 几何类型:-- 空间查询示例 SELECT * FROM locations WHERE ST_DWithin( geom, ST_GeomFromText('POINT(-71.060316 48.432044)', 4326), 1000 -- 1公里范围 ); 3. 事务隔离增强PostgreSQL通过MVCC实现四种隔离级别,其特色包括:可序列化快照隔离(SSI):SET default_transaction_isolation = 'serializable'; BEGIN; -- 并发事务不会因序列化失败而中止 UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; ** advisory locks**:-- 获取会话级建议锁 SELECT pg_advisory_xact_lock(12345); -- 业务逻辑处理... 三、性能优化实践1. 查询重写技巧物化视图自动更新:CREATE MATERIALIZED VIEW mv_sales_summary AS SELECT date_trunc('day', order_date) as day, SUM(amount) as total_sales FROM orders GROUP BY 1; -- 创建刷新策略 CREATE INDEX idx_mv_sales_day ON mv_sales_summary (day); CREATE UNIQUE INDEX ON mv_sales_summary (day); 部分索引优化:-- 仅索引活跃用户 CREATE INDEX idx_users_active ON users (email) WHERE is_active = true; 2. 执行计划分析使用EXPLAIN (ANALYZE, BUFFERS)获取详细执行信息:EXPLAIN (ANALYZE, BUFFERS) SELECT u.name, o.order_date FROM users u JOIN orders o ON u.id = o.user_id WHERE u.reg_date > '2025-01-01' ORDER BY o.order_date DESC LIMIT 100; 典型优化方向:调整work_mem参数影响排序性能通过parallel_setup_cost控制并行查询使用cluster命令按索引物理排序数据四、扩展语法特性1. 存储过程与函数PostgreSQL 11+支持的存储过程特性:CREATE OR REPLACE PROCEDURE transfer_funds( sender_id INT, receiver_id INT, amount NUMERIC ) LANGUAGE plpgsql AS $$ BEGIN -- 事务控制 UPDATE accounts SET balance = balance - amount WHERE id = sender_id AND balance >= amount; IF NOT FOUND THEN RAISE EXCEPTION 'Insufficient funds'; END IF; UPDATE accounts SET balance = balance + amount WHERE id = receiver_id; -- 记录审计日志 INSERT INTO transactions VALUES (...); END; $$; -- 调用存储过程 CALL transfer_funds(1, 2, 100.00); 2. 逻辑解码与CDC通过pg_logical扩展实现变更数据捕获:-- 创建发布端 SELECT * FROM pg_create_logical_replication_slot('my_slot', 'test_decoding'); -- 消费变更 SELECT data FROM pg_logical_slot_get_changes('my_slot', NULL, NULL); 五、未来趋势展望PostgreSQL 16+版本正在引入以下语法增强:并行哈希连接:提升大数据量连接性能增量排序:优化ORDER BY + LIMIT场景JSON路径查询增强:支持$[*]?(@.price > 100)语法Zheap存储引擎:实验性支持无MVCC开销的更新
  • [技术干货] PostgreSQL数据库高可用实现指南:从原理到生产实践
    PostgreSQL数据库高可用实现指南:从原理到生产实践在金融交易、物联网数据采集等场景中,数据库宕机每分钟可能造成数十万元损失。PostgreSQL凭借其强大的复制机制和丰富的生态工具,已成为企业级高可用架构的核心选择。本文将结合生产环境实践经验,深度解析PostgreSQL高可用的技术实现路径。一、高可用架构核心指标构建高可用系统需满足以下关键指标:RTO(故障恢复时间):核心系统要求<30秒自动切换RPO(数据丢失量):同步复制实现零数据丢失扩展能力:支持读写分离和在线扩容脑裂防护:具备自动仲裁机制某跨境电商平台通过Patroni+ETCD方案,将数据库可用性从99.9%提升至99.999%,全年故障恢复时间从8小时缩短至12分钟。二、原生流复制技术详解1. 基础配置示例# 主库postgresql.conf配置 wal_level = replica max_wal_senders = 10 synchronous_commit = remote_apply # 同步复制模式 hot_standby = on # 备库recovery.conf(PostgreSQL 12+使用standby.signal替代) primary_conninfo = 'host=192.168.1.101 port=5432 user=repl password=SecurePass123 application_name=node2' restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p' 2. 复制状态监控-- 实时监控复制延迟 SELECT pid, state, sync_state, pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS send_delay, pg_wal_lsn_diff(sent_lsn, write_lsn) AS write_delay, pg_wal_lsn_diff(write_lsn, flush_lsn) AS flush_delay, pg_wal_lsn_diff(flush_lsn, replay_lsn) AS replay_delay FROM pg_stat_replication; 3. 同步复制优化级联复制:通过中间节点降低跨机房带宽消耗复制插槽:防止WAL日志被过早回收-- 创建物理复制插槽 SELECT * FROM pg_create_physical_replication_slot('slot_node2'); 三、自动化故障转移方案1. Patroni+ETCD集群架构# patroni.yml核心配置 scope: pg-cluster namespace: /service/ name: node1 restapi: listen: 0.0.0.0:8008 connect_address: 192.168.1.101:8008 etcd: hosts: etcd1:2379,etcd2:2379,etcd3:2379 bootstrap: dcs: ttl: 30 loop_wait: 10 retry_timeout: 10 maximum_lag_on_failover: 1048576 # 1MB复制延迟阈值 生产环境优化点:集成pg_rewind实现异常节点自动修复通过watchdog硬件模块防止脑裂支持滚动升级和配置动态更新2. HAProxy负载均衡配置# haproxy.cfg示例 frontend pg-frontend bind *:5432 mode tcp default_backend pg-backend backend pg-backend mode tcp balance roundrobin option httpchk GET /master server node1 192.168.1.101:5432 check port 8008 server node2 192.168.1.102:5432 check port 8008 backup健康检查优化:使用/master接口检测主节点状态配置inter 2s实现秒级故障检测通过rise/fall参数控制切换灵敏度四、云原生高可用实践1. AWS RDS Multi-AZ部署架构特点:存储级同步复制(纳秒级延迟)内置健康检查API端点自动DNS切换机制按秒计费的日志传送带宽监控指标:DatabaseConnections:连接数突增预警ReplicationLag:复制延迟监控CPUUtilization:CPU使用率阈值告警2. Kubernetes Operator方案# PostgreSQLCluster CRD示例 apiVersion: postgres-operator.crunchydata.com/v1beta1 kind: PostgresCluster metadata: name: hippo spec: image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-15.6-0 postgresVersion: 15 instances: - name: instance1 replicas: 3 dataVolumeClaimSpec: accessModes: - ReadWriteOnce resources: requests: storage: 1Ti backups: pgbackrest: image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.45-0 repos: - name: repo1 volume: volumeClaimSpec: accessModes: - ReadWriteOnce resources: requests: storage: 1Ti优势特性:自动化的备份恢复流程动态扩容存储卷基于PDB的优雅驱逐自定义监控指标导出五、故障处理实战案例案例1:主库脑裂处置-- 1. 识别异常节点 SELECT usename, application_name, client_addr, state FROM pg_stat_activity WHERE state = 'active' AND usename != 'postgres'; -- 2. 强制终止异常连接 SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND backend_type = 'walsender'; 案例2:复制链路重建# 1. 基础备份重建 pg_basebackup -h new_primary -D /data/pg/standby -P -v -R -X stream -C -S slot_node2 # 2. 配置recovery.signal echo "standby_mode = 'on'" > /data/pg/standby/recovery.signal # 3. 启动备库 systemctl start postgresql-15案例3:DCS集群故障恢复# Patroni DCS故障重试机制 import etcd3 from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1)) def update_leader_key(key, value): etcd = etcd3.client(host='etcd1', port=2379) try: etcd.put(key, value) except etcd3.exceptions.EtcdConnectionFailed: raise # 触发重试机制 六、未来演进趋势AI运维集成:通过机器学习预测故障发生,实现预防性维护Service Mesh整合:利用Istio实现数据库流量的智能路由新硬件加速:采用NVMe SSD和RDMA网络降低复制延迟量子安全加密:部署PostgreSQL 16的量子抗性加密模块某银行核心系统改造实践显示,采用Patroni+Rook-Ceph存储方案后,数据库性能提升300%,同时将运维人力成本降低65%。建议企业每季度执行完整的容灾演练,确保高可用机制的有效性。构建高可用PostgreSQL集群需要综合考虑业务需求、技术储备和成本因素。通过合理选择技术方案,结合自动化运维工具,可以打造出满足金融级要求的数据库环境,为业务发展提供坚实的数据支撑。
  • [技术干货] 从原理到实战详解PostgreSQL如何进行性能优化 --转载
    一、技术背景与应用场景随着互联网业务的不断发展,数据量和并发访问量呈指数级增长,传统数据库面临着读写性能、连接吞吐、锁争用等多重挑战。PostgreSQL作为成熟的开源关系型数据库,以其丰富的特性和高扩展性受到广泛青睐。然而,在大规模生产环境中,如果不对其内部原理与配置参数进行深入理解并合理调优,往往难以发挥其最佳性能。常见的应用场景包括:高并发在线事务处理(OLTP):电商下单、支付结算等场景对响应时间要求严格。复杂分析查询(OLAP):报表查询、BI分析对大表扫描和聚合性能提出挑战。混合负载场景:同时承担写入与分析查询,要求数据库在多种负载模式下稳定表现。本文将从核心原理入手,结合配置参数、索引策略与SQL执行计划,提供可复用的实践示例与优化建议。二、核心原理深入分析2.1 PostgreSQL体系架构PostgreSQL采用多进程模式而非线程,主要组件包括:Postmaster(主进程):负责监听连接、管理子进程。Backend(会话进程):每个客户端连接对应一个后台进程,处理执行请求。Shared Buffer Pool:共享内存区,用于缓存数据页;大小由 shared_buffers 参数控制。WAL(Write-Ahead Logging):事务日志保证持久性;配置 wal_level、checkpoint_segments 影响写盘与恢复性能。图示简化架构:  Client ---> Postmaster ---> Backend Process ---> Shared Buffers <--> Storage                                         |                                         +--> WAL Log2.2 查询执行引擎与计划选择执行流程:解析(Parser)→ 重写(Rewriter)→ 优化器(Planner/Optimizer)→ 执行器(Executor)。解析/重写负责语法检查和视图/规则替换。优化器基于成本模型(Cost Model)选择最优执行计划,包括顺序扫描、索引扫描、排序后合并等。参数 random_page_cost、seq_page_cost、cpu_tuple_cost 等影响估算成本。示例:使用 EXPLAIN (ANALYZE, BUFFERS) 查看执行计划:123456EXPLAIN (ANALYZE, BUFFERS)SELECT u.id, u.name, o.totalFROM users uJOIN orders o ON u.id = o.user_idWHERE u.status = 'active'  AND o.created_at >= now() - interval '30 days';输出重点:Seq Scan vs Index Scan、Buffers: shared hit、Actual time。三、关键源码解读3.1 shared_buffers和work_memshared_buffers:缓冲池大小,建议设置为总内存的 1/4 ~ 1/2。work_mem:每个排序/哈希操作的内存限制,设置过小会导致磁盘排序,过大则可能占满内存。在源码 src/backend/utils/memutils/ 中,内存上下文(MemoryContext)负责动态分配:12345/* MemoryContext分配示例 */MemoryContext oldcontext;oldcontext = MemoryContextSwitchTo(work_mem_context);ptr = MemoryContextAlloc(work_mem_context, size);MemoryContextSwitchTo(oldcontext);3.2 WAL和Checkpoint机制WAL日志写入路径:事务提交 → 写入WAL缓冲区 → 调用 XLogFlush 强制刷盘。源码逻辑位于 src/backend/access/transam/xlog.c:1234/* 写WAL */RedobackupBlock(blk);XLogInsert(RM_XACT_ID, XLOG_XACT_COMMIT);XLogFlush(record_ptr);checkpoint_segments 与 checkpoint_timeout 决定Checkpoint频率,过高可降低IO压力但恢复时间增长。四、实际应用示例4.1 OLTP场景下性能调优调整 shared_buffers = 8GB,work_mem = 64MB。设置 effective_cache_size = 24GB,帮助优化器评估可用缓存。限制 max_connections = 200,配合连接池(PgBouncer)减少进程开销。示例SQL调整:12345ALTER SYSTEM SET shared_buffers = '8GB';ALTER SYSTEM SET work_mem = '64MB';ALTER SYSTEM SET effective_cache_size = '24GB';ALTER SYSTEM SET max_connections = 200;SELECT pg_reload_conf();4.2 大表聚合查询优化对于大表上的聚合和排序,可采用:分区表:按时间/范围分区,查询时只扫描相关分区。物化视图:预计算热点报表数据,定时刷新。12345678910111213-- 创建分区表示例CREATE TABLE orders_2023 PARTITION OF orders  FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); -- 物化视图示例CREATE MATERIALIZED VIEW mv_monthly_sales ASSELECT date_trunc('month', created_at) AS month,       SUM(total) AS total_salesFROM ordersGROUP BY 1; -- 刷新视图REFRESH MATERIALIZED VIEW CONCURRENTLY mv_monthly_sales;五、性能特点与优化建议监控与分析:结合 pg_stat_statements、EXPLAIN,持续跟踪慢查询与热点表。IO优化:部署高速SSD,针对写密集型场景可调整 wal_compression、开启异步提交。缓存策略:合理设置 shared_buffers 与 effective_cache_size,配合 OS 缓存。索引设计:避免冗余索引,针对常用查询列创建部分索引与表达式索引。分区与表维护:使用表分区、定期 VACUUM ANALYZE,清理死锁并更新统计信息。
  • [技术干货] Windows 安装 PostgreSQL 并安装 vector 扩展的流程 --转载
    一、PostgreSQL 安装流程获取安装包访问官网,选择与系统匹配的最新版本安装程序执行安装程序双击运行安装向导选择默认安装路径 C:\Program Files\PostgreSQL\<版本号>设置超级用户密码(需包含大小写字母和数字)确认端口号为 5432(默认值,建议保持)验证服务状态打开 PowerShell 执行:1Get-Service postgresql* | Select-Object Name, Status正常状态应显示 Running二、环境配置优化添加系统路径将 PostgreSQL 工具路径加入环境变量:1C:\Program Files\PostgreSQL\<版本号>\bin操作步骤:系统属性 → 高级 → 环境变量 → Path 编辑字符编码设置在 IDE 或应用配置中添加(如需处理中文):1-Dfile.encoding=UTF-8三、Vector 扩展部署编译环境准备安装 PostgreSQL安装 PostgreSQL 开发包(安装时勾选 PGXS 组件)**扩展安装步骤1234git clone --depth 1 https://github.com/pgvector/pgvector.gitcd pgvectornmake /F Makefile.windowsnmake /F Makefile.windows install四、数据库配置创建专属数据库12345CREATE DATABASE vectordb   TEMPLATE template0   ENCODING 'UTF8'  LC_COLLATE 'C'  LC_CTYPE 'C';启用向量扩展12\connect vectordbCREATE EXTENSION IF NOT EXISTS vector;五、功能验证测试扩展状态检查1SELECT * FROM pg_extension WHERE extname = 'vector';向量操作演示12345678910CREATE TABLE embeddings (  id SERIAL PRIMARY KEY,  feature VECTOR(768));INSERT INTO embeddings (feature) VALUES ('[0.12, 0.34, ..., 0.78]'),       ('[0.56, 0.91, ..., 0.22]');SELECT id, feature <-> '[0.23, 0.45, ..., 0.67]' AS distance FROM embeddings ORDER BY distance LIMIT 5;六、故障排查指南问题现象解决方案扩展加载失败检查 data/postgresql.conf 中 shared_preload_libraries = 'vector'权限拒绝错误使用 psql -U postgres -h 127.0.0.1 连接向量运算异常确认 vector.so 文件位于 lib/postgresql 目录日志路径参考C:\Program Files\PostgreSQL\<版本号>\data\log\postgresql-<日期>.log注意事项建议使用 PostgreSQL 15 及以上版本进行向量运算时确保内存 ≥ 8GB开发环境建议关闭 ssl 减少性能损耗
总条数:137 到第
上滑加载中