-
已经创建 OBS,ECS,使用 ECS 在线下载数据集到 OBS,但是有 50 个 T请问如何加速 ECS 下载速度,尤其是到外网的速度
-
我们打算测试一下310p的推理,但是两种租算力的方式: Notebook 以及轻算力节点,搜了一下各个区域,都没有找到310的npu算力。请问有310p算力吗?
-
为什么售罄了,请问什么时候能补货?不管是哪个节点,有关NPU全部售罄了,好奇怪啊
-
这是我的代金券说明,我可以用来买华为云弹性云服务器等其他服务器吗?想租Ascend910的卡只能在ModelArts里面的实例租吗?我看弹性云服务器里面的实例都是NVIDIA Tesla系列等等,并没有Ascend系列。
-
如图这是我的代金券的说明,我想问下可以用于购买弹性云服务器等其他服务器吗?想租Ascend910b只能在modelArts里面租实例吗?弹性云服务器里面的实例里面的GPU都是NVIDIA Tesla系列 并没有Ascend系列的
-
尝试使用: &model.NovaAssociateSecurityGroupRequestBody和&model.NovaDisassociateSecurityGroupRequestBody来实现安全组的更改,但是提示NovaDisassociateSecurityGroupRequestBody:The API does not exist or has not been published in the environment。又试了使用UpdatePort来更改属性,但是也报错:The API does not exist or has not been published in the environment nicId, err := c.getPrimaryNicId(instanceId) request := &vpcModel.UpdatePortRequest{ PortId: nicId, Body: &vpcModel.UpdatePortRequestBody{ Port: &vpcModel.UpdatePortOption{ SecurityGroups: &[]string{NewSegId}, }, }, } _, err = client.UpdatePort(request) 找了很多api,但是没找到github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.168这个版本可用的api
-
弹性云服务器基于虚拟化技术构建,单实例可视为一台虚拟机,但其本质是具备弹性伸缩、按需付费特性的云服务,远超传统虚拟机范畴。弹性云服务器共担模型:云平台保障底层基础设施和虚拟化层安全;用户则需负责操作系统加固、应用安全、防火墙(安全组)配置及数据加密。平台本身提供安全基础,但最终安全性高度依赖用户的安全运维实践。因此,它既是安全的,也存在风险,安全与否主要取决于用户自身的配置与管理水平。 一、弹性云服务器是虚拟机吗?要回答这个问题,我们需要从技术和演进两个层面来理解。1. 从技术实现上看:是的,但非传统意义上的虚拟机弹性云服务器在Hypervisor(虚拟机监视器)之上创建和运行。Hypervisor是一种将物理服务器硬件(CPU、内存、存储、网卡)进行虚拟化,并抽象为多个独立、隔离的虚拟硬件环境的软件层。每个云服务器实例都运行在这样一个虚拟化的硬件环境中,拥有自己的操作系统(Guest OS)。从这个角度看,单台弹性云服务器的诞生方式与传统VMware、Hyper-V创建的虚拟机(VM)在技术原理上同源。2. 从演进与赋能上看:不是,它是云时代的革命性产品尽管技术同源,但将云服务器简单等同于传统虚拟机是片面且过时的。二者的核心差异在于“弹性”与“管理”模式:资源形态:传统虚拟机资源固定(如分配4核8G,则长期独占该资源),而云服务器的资源是池化、弹性可伸缩的。您可以随时按需升降配CPU、内存,甚至无需重启,这是传统虚拟机难以企及的。获取方式:传统虚拟机需自行部署硬件和虚拟化软件,耗时数天甚至数周。云服务器则通过Web控制台或API秒级交付,即刻可用。运维模式:传统虚拟机需要用户自己维护底层的物理服务器、Hypervisor和虚拟化网络。而云服务器由云厂商负责所有底层基础设施的维护、冗余和稳定性,用户**专注于实例内部的应用,实现了运维的极大简化。结论:弹性云服务器基于虚拟化技术,但其价值远超越传统虚拟机。它是集成了计算、网络、存储的云服务单元,其“弹性”、“按需”和“自助服务”的特性,是传统虚拟机概念在云计算时代的一次质的飞跃。二、安全性全景透视:从技术栈到管理闭环🔍 常见担忧溯源用户对安全性的焦虑主要集中于三点:① 多租户环境下的数据隔离风险;② 网络攻击面的扩大;③ 服务商自身的可靠性。事实上,主流云厂商已构建起多层防护体系,但这些措施往往因技术门槛而被忽视。🛡️ 六大安全防线拆解硬件级隔离采用Intel VT-d/AMD-Vi等芯片级虚拟化技术,确保不同租户的计算资源完全隔离,即使同一物理机上的其他用户遭受攻击,也不会影响您的业务。网络边界防护VPC(虚拟私有云)划分独立网络空间,配合安全组规则精细控制入站/出站流量;NAT网关隐藏内网IP,防止公网直接暴露;Web应用防火墙(WAF)拦截SQL注入、XSS等常见攻击。数据加密全链路覆盖传输层强制TLS加密,存储层支持国密算法SM4/RSA双重加密;密钥管理系统(KMS)实现敏感数据的权限分级管控。身份与访问管理(IAM)多因素认证(MFA)替代弱密码;细粒度的角色权限分配,限制高危操作;操作日志全程审计,异常行为实时告警。灾难恢复体系跨可用区的数据冗余备份,RPO接近零;快照功能可快速回滚至任意时间点;冷备数据中心应对区域性灾害。合规与审计通过ISO 27001、等保三级等认证,定期接受第三方渗透测试,并提供完整的合规报告。⚠️ 警惕隐性风险尽管云服务商提供了完善的基础设施,但最终安全仍取决于用户的配置策略。例如:未及时修补系统漏洞、过度开放端口权限、弱密码策略等,均可能导致安全事件。建议遵循“最小权限原则”,仅开放必要端口,并启用自动化补丁更新。三、如何选择可靠的弹性云服务?📊 决策checklist 💡 实操建议压力测试先行:模拟真实业务负载,验证弹性扩缩容的稳定性;混合云过渡:将非核心业务迁移至云端,逐步建立信任;持续监控:利用云平台的监控工具,设置CPU/内存/带宽的使用阈值告警。总结弹性云服务器是虚拟机吗?它是虚拟化技术的产物,但更是超越了传统虚拟机概念的、具备弹性伸缩能力的云计算服务单元。安全吗?云平台提供的底层基础设施本身是高度安全的。但最终的安全性取决于用户如何管理和配置自己的云服务器。云服务器提供了构建安全环境的基础工具和能力,但“安全”更像是一个需要用户持续维护的状态,而非一个一劳永逸的属性。因此,选择一家技术雄厚、运维规范的云平台是基础,而用户自身具备良好的安全意识和运维习惯,才是保障弹性云服务器安全最坚固的防线。
-
[技术干货] 【ECS 资源管理与运维体验优化升级】监控大盘全新上线、可自由集成云服务总览、监控中心、实例监控详情相关的核心指标数据,资源拓扑图一站式查看关联资源,新增支持子网ID、镜像ID、资源标签、IPV6地址筛选条件,支持实例名称模糊搜索。一、新增功能1、监控大盘全新上线、可自由集成云服务总览、监控中心、实例监控详情相关的核心指标数据2、资源拓扑图一站式查看关联资源二、体验优化1、新增支持子网ID、镜像ID、资源标签、IPV6地址筛选条件,支持实例名称模糊搜索
-
[技术干货] 【ECS 资源配置操作优化升级】支持批量切换/重装操作系统、批量切换VPC、批量编辑标签能力,购买页新增规格对比能力、删除云服务器支持选择同步删除云服务器关联的云硬盘、弹性公网IP、弹性负载均衡后端服务器等相关资源。一、新增功能1、新增支持批量切换/重装操作系统、批量切换VPC、批量编辑标签。定时切换/重装操作系统、定时变更规格快马加鞭上线中二、体验优化1、购买页新增支持规格对比能力2、删除云服务器支持选择同步删除云服务器关联的云硬盘、弹性公网IP、弹性负载均衡后端服务器等相关资源
-
余年方十七,习编程已有数载,虽非初学,然深知学无止境,技艺之精进,如逆水行舟,不进则退。 技艺渐精 昔日初学时,常为类型错误而苦恼,今已能熟练运用多种语言。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">需努力 (<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)
-
Roxaew勒索病毒篡改重要文件1. 篡改重要文件2. SQLSERVER数据库被锁定3. SQLSERVER备份文件被修改请帮忙,感谢各位
-
优化点:ECS列表搜索框筛选条件增加子网ID/镜像ID/资源标签/IPV6地址等,名称/规格名称/私有IP地址搜索支持模糊搜索 加速体验优化【操作步骤】一、ECS列表搜索框支持子网ID/镜像ID/资源标签/IPV6地址等筛选条件搜索1 进入云服务器控制台2 单击弹性云服务器控制台列表上方搜索输入框,选择子网ID/镜像ID/资源标签/IPV6地址等筛选条件,输入搜索内容 【其他】发布地域:均已上线帮助文档:cid:link_1 二、ECS列表搜索框支持名称/规格名称/私有IP地址模糊搜索【操作步骤】1 进入云服务器控制台2 单击弹性云服务器控制台列表上方搜索输入框,选择名称/规格名称/私有IP地址,输入模糊描述内容【其他】发布地域:均已上线帮助文档:cid:link_1 收集使用反馈如果您在使用过程中有具体的意见或建议,可以通过华为云控制台ECS调研问卷反馈给我们,产品小哥会第一时间进行优化分析!
-
优化点:ECS删除时可以同步删除关联的EIP和数据盘,简化删除步骤,避免未删除的EIP和数据盘持续计费;编辑/添加/删除ECS标签时支持选择同步编辑关联的云硬盘和EIP资源标签,优化标签管理 加速体验优化 【操作步骤】一、支持关联删除ECS绑定的弹性公网IP地址和挂载的数据盘单操作入口 1:进入云服务器控制台,选择单台需要删除的弹性云服务器ECS,在更多列选择“删除”,在弹窗中是否同步删除关联资源下,勾选需要同步删除的关联资源,点击“下一步”即可入口 2:进入云服务器控制台,单击弹性云服务器名称进入云服务器详情页面,点击上方更多按钮,在下拉列表中选择“删除”, 在弹窗中是否同步删除关联资源下,勾选需要同步删除的关联资源,点击“下一步”即可批量操作1 进入云服务器控制台,选择多台需要删除的弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“删除”,在弹窗中是否同步删除关联资源下,勾选需要同步删除的关联资源,点击“下一步”即可【其他】发布地域:均已上线 二、支持编辑ECS标签时同步编辑关联的云硬盘和弹性公网IP资源标签【操作步骤】单操作入口 1:进入云服务器控制台,选择单台需要编辑标签的弹性云服务器ECS,点击”标签”的“编辑标签”,在弹窗中选择添加/编辑/删除标签,勾选需要同步编辑关联资源标签,点击“确定”即可入口 2:进入云服务器控制台,单击弹性云服务器名称进入云服务器详情页面,点击“标签”进入标签详情页面,点击添加/编辑/删除标签,在弹窗中勾选需要同步添加/编辑/删除关联资源标签,点击“确定”即可批量操作1 进入云服务器控制台,选择多台需要编辑标签的弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“编辑标签”,在弹窗中选择添加/编辑/删除标签,勾选需要同步编辑关联资源标签,点击“确定”即可【其他】发布地域:均已上线 收集使用反馈 如果您在使用过程中有具体的意见或建议,可以通过华为云控制台ECS调研问卷反馈给我们,产品小哥会第一时间进行优化分析!
-
倾听客户声音客户A:有一批ECS虚拟机需要重装系统,控制台只能一台一台去操作,目前无法批量去重装系统,建议可以做个批量按钮客户B:是否有批量切换操作系统的选项,如果没有的话,可否添加这样的需求,我们后端机器都是同一个镜像,有时会根据需要更新程序,批量更换镜像客户C:目前需要大批量切换VPC,但是控制台只能一台一台切换,需要批量切换VPC功能客户D:客户要修改ECS资源的关联标签,只能一个一个去修改,需要按钮能批量的修改关联资源标签 加速体验优化【操作步骤】一、ECS支持批量重装操作系统1 进入云服务器控制台2 选择多台弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“重装操作系统”3 设置批量重装操作系统策略,点击“确定”即可【其他】发布地域:均已上线 二、ECS支持批量切换操作系统1 进入云服务器控制台2 选择多台弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“切换操作系统”,选择云服务器相匹配的计费模式3 设置批量切换操作系统策略,点击“确定”即可【其他】发布地域:均已上线 三、ECS支持批量切换VPC【操作步骤】1 进入云服务器控制台2 选择多台弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“切换VPC”【其他】发布地域:均已上线帮助文档:cid:link_1 四、ECS支持批量编辑/添加/解绑标签【操作步骤】1 进入云服务器控制台2 选择多台弹性云服务器ECS,点击列表上方“更多”按钮,在下拉列表中选择“编辑标签”3 对所选弹性云服务器ECS编辑/添加/删除标签,点击“确定”即可【其他】发布地域:均已上线 收集使用反馈如果您在使用过程中有具体的意见或建议,可以通过华为云控制台ECS调研问卷反馈给我们,产品小哥会第一时间进行优化分析!
-
案例介绍本案例基于免费的华为云开发者空间、华为云MaaS提供的DeepSeek-R1大模型,使用R语言工具构建AI Agent,该AI Agent可以在R语言环境下独立部署使用。也可以用R语言开发MCP server,和AI助手平台Cherry Studio配合使用。案例内容本案例由开发者:yd_259866430提供案例内容1 概述1.1 案例介绍R是一种自由、免费、源代码开放的计算机语言。它具有语法极其简单、代码量极少、没有复杂概念的显著优势,降低了人们了解更复杂技术的计算机语言门槛。本案例将介绍如何在华为云主机安装R,并用它开发AI Agent和MCP server。本案例用到的R语言工具包括ellmer和mcptools,是posit公司开发的智能体开发工具。如需在R中开发部署AI Agent,则用ellmer。如果R仅作为MCP server的运行环境,则使用mcptools。1.2 适用对象企业个人开发者高校学生1.3 案例时间本案例总时长预计60分钟。1.4 案例流程① 用户打开华为开发者空间云主机,下载安装R、ellmer、mcptools、Cherry Studio;② 使用ellmer包,配置华为云MaaS提供的DeepSeek-R1模型,运行测试AI Agent;③ 使用mcptools开发MCP server服务,并将MCP server部署在Chery Studios上,配置华为云MaaS提供的DeepSeek-R1模型,运行测试AI Agent;1.5 资源总览本案例预计花费0元。资源名称规格单价(元)时长(分钟)鲲鹏4vCPUs |8GB ARM|Ubuntu 24.04 Server定制版0602 环境配置2.1 开发者空间配置详细步骤请参考案例中步骤2.12.2 免费领取DeepSeek-R1详细步骤请参考案例中步骤2.22.3 安装R语言2.3.1 进入开发者云主机桌面后,打开一个终端2.3.2 在终端输入以下代码,安装R语言sudo apt update sudo apt install --no-install-recommends software-properties-common dirmngr wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | sudo tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc sudo add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/" sudo apt install --no-install-recommends r-base具体参考R官网安装指引。https://mirrors.sustech.edu.cn/CRAN/在终端中输入R,进入R console中,输入print("hello world!"),如正常输出,则R语言安装成功。2.4 安装ellmer和mcptools在R console中输入install.packages("ellmer")过程中报错,查看报错信息按照报错信息,先退出R console,在终端中安装libcurl4-openssl-dev。再进入R console,安装ellmer在R console中,安装usethis(用于配置R环境变量),install.packages("usethis")安装过程会报错,需要退出R console安装libgit2-dev,sudo apt install libgit2-dev,然后再进入R console安装usethis。然后,安装install.packages("mcptools"),过程中一样会报错,需要耐心debug。2.5 安装Cherry Studiosudo apt install gdebi-core sudo gdebi Cherry-Studio-1.5.3-arm64.deb3 构建AI Agent3.1 使用ellmer创建AI Agent在R console输入以下代码,编辑R环境变量library(ellmer) usethis::edit_r_environ() 将参考案例中步骤2.2中的url和api_key复制到R环境变量中,复制后,按:wq退出,和vim语法类似。在 R console输入如下命令library(ellmer) # 导入ellmer chat <- chat_openai( # 构建AI Agent system_prompt = NULL, base_url = Sys.getenv("base_url"), api_key = Sys.getenv("api_key"), model = "DeepSeek-R1", seed = NULL, api_args = list(), echo = "all" ) 3.2 测试AI Agent在R console里,输入chat$chat("讲三个关于统计学家的笑话") ,就可以看到这个AI Agent确实通过API连接上了华为云上Deepseek模型,并返回了结果。在R console里运行AI Agent,看起来很丑,其实posit公司开发了更好看的shinychat,如下图所示。但是安装部署shinychat,又是一个不小的工程,所以案例接下来转向R+Cherry Studio 配合使用。不过聊天仅仅是这个AI Agent一个很小的功能。作为这个AI Agent的使用者,我们更希望它能够利用由我们定义的外部工具。这不仅仅是希望,如果AI Agent不调用外部工具,它的回答可能是错误的。如下图所示,当我们问AI Agent今天是几号,在不调用外部工具的情况下,它给出的是2023年10月12日,这明显不对。3.3 使用mcptools开发MCP server,并配合Cherry Studio使用解决“今天几号”的问题,需要用mcptools开发一个MCP server,写一个mcptoolsExample.R脚本,代码如下。get_current_time <- function(tz = "CST") { format(Sys.time(), tz = tz, usetz = TRUE) } if (is_new_ellmer()) { res <- list( get_current_time = ellmer::tool( get_current_time, name = "get_current_time", description = "返回当前时间", arguments = list( tz = ellmer::type_string( "当前时区默认为 `\"CST\"`.", required = FALSE ) ) ) ) } else { res <- list( get_current_time = ellmer::tool( get_current_time, name = "get_current_time", description = "Returns the current time.", arguments = list( tz = ellmer::type_string( "当前时区默认为 `\"CST\"`.", required = FALSE ) ) ) ) } res然后,我们按照参考案例,在Cherry Studio上配置DeepSeek-R1模型。如下图所示。然后,在不配置MCP server的情况,我们再问下“今天是几号”。AI Agent无法回答这个问题。接下来,我们给这个AI Agent配上一个外部工具,如下图所示,点击添加服务器。在json中输入{ "mcpServers": { "r-mcptools": { "command": "Rscript", "args": ["-e", "mcptools::mcp_server("/path/mcptoolsExample.R")"] } } } 接下来,再次问“今天是几号”,AI Agent已经知道调用MCP server获取日期了。至此,基于华为开发者空间云主机、R语言搭建AI Agent和MCP Server到此结束。本案例使用华为开发者空间提供的免费主机作为开发环境,操作系统为华为云为新注册用户提供180小时免费主机,大家可以开心使用。本案例使用华为云MaaS提供的免费DeepSeek-R1模型。我正在参加【案例共创】第5期 开发者空间 AI Agent 开发 https://developer.huaweicloud.com/signup/0e966198e87a4210b5f88c0d759d4f3b”
上滑加载中
推荐直播
-
用码道,让你的AI作品三步上朋友圈2026/08/04 周二 19:00-20:00
林华鼎-华为云AI开发者运营负责人
从入门 · 到做AI应用 · 到企业级开发。不教编程,只教用AI · 零代码、有产出、能带走、可炫耀 · 每课人人动手实操
回顾中 -
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
基于华为云码道,构建你的定制化AI搭子2026/08/14 周五 09:00-11:30
明亮-华为云开发者发展与支持部部长
本期直播将向您全面介绍华为云码道产品,并基于码道手把手教你部署自己的定制化AI陪伴搭子。
回顾中
热门标签