-
一、概述1. 案例介绍本文通过本地IDEA连接开发者云开发环境,通过本地的IDEA来进行远程编程并在java中连接GaussDB数据库来构建应用。2. 适用对象企业个人开发者高校学生3. 案例时间本案例总时长预计60分钟 说明:登陆开发者空间-创建云开发环境-下载连接客户端;通过客户端获取开发环境实例;通过idea建立隧道连接云开发环境;通过IntelliJ IDEA完成代码编写、编译、运行。 5. 资源总览本案例预计花费1元。资源名称单价(元)时长(分钟)华为开发者空间 - 云开发环境免费40GaussDB免费10弹性公网110 二、环境配置1. 云开发环境配置通过在开发者空间-开发平台-云开发环境模块查看开发环境相关内容。对于未创建 开发环境的同学,点击《新建开发环境》在下图长方形标注区域会让填写相关信息,填写完成后则完成创建。 创建成功后,点击更多按钮进行《开机》操作开机完成后则可以下载完成连接软件进行连接2、云环境连接下载完成连接软件后,通过cmd命令进入到软件目录,输入命令进行测试查看hdspace.exe devenv --help输入下方命令进行实例查询hdspace.exe devenv list输入上面命令后,需要输入相应的key id和access key.按以下步骤则可以创建一个访问秘钥,创建成功后下载即可。开始连接输入命令:开启连接hdspace.exe devenv start-tunnel -instance-id=实例名称来源于上面list命令返回通过IDEA进行远程连接在IDEA中点击File - Remote Development 按钮在开启的弹窗 中点击 SSH - New Project 创建一个项目需要输入远程连接的参数,如无其他配置,则按下图输入即可 确认连接后,会弹出连接密码输入框 ,正确输入即可正确输入后会弹出警告框,点击 continue Anyway即可成功连接后,选择开发目录在弹出的输入密码框中再次输入密码,然后在弹出的警告框再次跳过全部这样,远程主机就开始下载idea了。下载成功后,会自动开启一个idea,我们可以查看项目目录就是我们选择的目录了。 三、使用云开发环境完成代码开发1. 新建远程项目通过IDEA创建一个新项目,注意location按我下方 的写,本地路径是在/home/developer下面。创建java项目完成后,则可以看到相应代码目录 四、开通GaussDB数据库申请GaussDB数据库后,进 入开发者空间就会提示已获得,立即开通开通后,点击立即前往可看到数据库正在创建安装成功后,点击登录,在跳转登录页面输入密码,点击测试连接,通过后,确定即可登录成功登录成功说明数据库就没有问题了,我们给数据库绑定一下公网ip选择按需计费,宽带大小按自己要求即可提交后就有弹性公网ip了,这时我们回到数据库进行绑定。选择新创建的ip进行绑定绑定好后就可以连接数据库了,下面演示通过navicat连接 五、抽奖小应用通过IDEA的命令行,创建抽奖应用类,增加依赖包 <dependencies> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> <!-- 使用适合您环境的版本 --> </dependency> </dependencies>package org.example;import java.sql.*;import java.util.Random;import java.util.Scanner;public class Choujiang { // 数据库连接信息 - 请替换为您的实际信息 private static final String JDBC_URL = "jdbc:postgresql://113.46.208.183:8000/xiaocao"; private static final String USERNAME = "root"; private static final String PASSWORD = "xiaocao_123"; // 奖品配置 private static final String[] PRIZES = { "谢谢参与", "优惠券10元", "优惠券50元", "华为云代金券100元", "华为手机", "华为笔记本电脑" }; // 奖品概率(总和100) private static final int[] PRIZE_PROBABILITY = {40, 30, 15, 10, 4, 1}; private static Connection connection; private static Scanner scanner = new Scanner(System.in); private static Random random = new Random(); public static void main(String[] args) { try { // 初始化数据库 initDatabase(); System.out.println("=== 欢迎来到华为云幸运抽奖系统 ==="); while (true) { System.out.println("\n请选择操作:"); System.out.println("1. 用户注册"); System.out.println("2. 开始抽奖"); System.out.println("3. 查看我的中奖记录"); System.out.println("4. 退出"); System.out.print("请输入选择: "); int choice = scanner.nextInt(); scanner.nextLine(); // 消耗换行符 switch (choice) { case 1: registerUser(); break; case 2: drawPrize(); break; case 3: viewRecords(); break; case 4: System.out.println("感谢使用,再见!"); return; default: System.out.println("无效的选择,请重新输入!"); } } } catch (Exception e) { e.printStackTrace(); } finally { closeResources(); } } // 初始化数据库 private static void initDatabase() throws Exception { Class.forName("org.postgresql.Driver"); connection = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD); // 创建用户表 String createUserTable = "CREATE TABLE IF NOT EXISTS users (" + "id SERIAL PRIMARY KEY, " + "username VARCHAR(50) UNIQUE NOT NULL, " + "phone VARCHAR(20) UNIQUE NOT NULL, " + "register_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ")"; // 创建抽奖记录表 String createRecordTable = "CREATE TABLE IF NOT EXISTS prize_records (" + "id SERIAL PRIMARY KEY, " + "user_id INTEGER REFERENCES users(id), " + "prize_name VARCHAR(100) NOT NULL, " + "draw_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ")"; try (Statement stmt = connection.createStatement()) { stmt.execute(createUserTable); stmt.execute(createRecordTable); System.out.println("数据库初始化完成"); } } // 用户注册 private static void registerUser() throws SQLException { System.out.println("\n=== 用户注册 ==="); System.out.print("请输入用户名: "); String username = scanner.nextLine(); System.out.print("请输入手机号: "); String phone = scanner.nextLine(); String sql = "INSERT INTO users (username, phone) VALUES (?, ?)"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, phone); pstmt.executeUpdate(); System.out.println("注册成功!"); } catch (SQLException e) { System.out.println("注册失败: " + e.getMessage()); } } // 抽奖 private static void drawPrize() throws SQLException { System.out.println("\n=== 幸运抽奖 ==="); System.out.print("请输入您的手机号: "); String phone = scanner.nextLine(); // 查询用户ID Integer userId = getUserIdByPhone(phone); if (userId == null) { System.out.println("未找到该用户,请先注册!"); return; } // 随机抽奖 int prizeIndex = getRandomPrizeIndex(); String prize = PRIZES[prizeIndex]; // 记录中奖信息 String sql = "INSERT INTO prize_records (user_id, prize_name) VALUES (?, ?)"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { pstmt.setInt(1, userId); pstmt.setString(2, prize); pstmt.executeUpdate(); } System.out.println("\n恭喜您!"); System.out.println("您获得了: " + prize); System.out.println("奖品已记录到您的账户"); } // 根据概率随机获取奖品索引 private static int getRandomPrizeIndex() { int randomNum = random.nextInt(100) + 1; int range = 0; for (int i = 0; i < PRIZE_PROBABILITY.length; i++) { range += PRIZE_PROBABILITY[i]; if (randomNum <= range) { return i; } } return 0; // 默认返回"谢谢参与" } // 根据手机号获取用户ID private static Integer getUserIdByPhone(String phone) throws SQLException { String sql = "SELECT id FROM users WHERE phone = ?"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { pstmt.setString(1, phone); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { return rs.getInt("id"); } } return null; } // 查看中奖记录 private static void viewRecords() throws SQLException { System.out.println("\n=== 我的中奖记录 ==="); System.out.print("请输入您的手机号: "); String phone = scanner.nextLine(); Integer userId = getUserIdByPhone(phone); if (userId == null) { System.out.println("未找到该用户,请先注册!"); return; } String sql = "SELECT prize_name, draw_time FROM prize_records WHERE user_id = ? ORDER BY draw_time DESC"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { pstmt.setInt(1, userId); ResultSet rs = pstmt.executeQuery(); System.out.println("\n中奖记录:"); System.out.println("-----------------------------"); boolean hasRecord = false; while (rs.next()) { hasRecord = true; String prize = rs.getString("prize_name"); Timestamp time = rs.getTimestamp("draw_time"); System.out.printf("奖品: %-15s 时间: %tF %tT%n", prize, time, time); } if (!hasRecord) { System.out.println("暂无中奖记录,快去抽奖吧!"); } System.out.println("-----------------------------"); } } // 关闭资源 private static void closeResources() { try { if (connection != null) connection.close(); if (scanner != null) scanner.close(); } catch (SQLException e) { e.printStackTrace(); } }}这个时间直接运行,会报下面错误这时候我们有多种解决方案,其中一种如下,修改数据库的验证方式。执行代码 至此就完成了从云开发环境 到 GaussDB 创建应用连接的全部流程了。 我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 cid:link_1
-
基于华为开发者空间云开发环境和GaussDB数据库—爬取小说存入GaussDB数据库实战案例介绍本案例演示如何在华为开发者空间的远程云开发环境中,使用 Python 爬取网络小说数据,并将数据存入 GaussDB 云数据库。通过该实践,开发者能够掌握以下技能:配置并使用云开发环境进行 Python 项目开发;使用爬虫技术抓取网页小说内容;数据清洗与结构化处理;连接并操作 GaussDB 数据库,实现小说数据存储;实现基础的数据查询和管理功能。通过此案例,开发者可快速搭建一个完整的数据采集与存储流程,并体验云端开发与数据库结合的高效开发模式。案例内容本案例内容包括:在华为开发者空间创建云开发环境并配置 Python 运行环境;安装爬虫所需的第三方库,如 requests、BeautifulSoup 等;申请并连接 GaussDB 数据库实例;编写爬虫脚本抓取小说章节及内容;将抓取的小说数据存入 GaussDB 数据库;在数据库中验证数据存储和查询结果。最终效果:成功抓取指定网站小说内容;数据结构化存储在 GaussDB 数据库中;可通过 SQL 查询小说、摘要信息。一、概述在互联网内容获取和数据分析中,爬虫技术 是获取结构化数据的主要手段之一。而现代云开发环境与数据库服务,为爬虫项目提供了高效、稳定、安全的执行平台。本案例通过 Flask 无关的纯爬虫 + 数据库操作方式,展示如何在云端环境完成小说数据的抓取与存储。使用 GaussDB 数据库,开发者无需担心本地数据库配置、权限问题或数据备份,同时结合云开发环境,支持团队协作和远程操作。1.1 案例介绍本案例将演示以下操作流程:使用 Python 爬取指定小说网站的章节标题与内容;对抓取内容进行清洗,剔除无用标签和广告文本;将小说数据以结构化形式存入 GaussDB 数据库,包括章节号、标题、正文和更新时间;使用 SQL 验证数据完整性和正确性。通过实践,开发者可以快速搭建一个简单的 云端小说数据库管理系统,为后续的阅读分析、内容推荐或搜索功能提供基础数据。1.2 适用对象个人开发者,尝试云端爬虫与数据库存储项目;高校学生,学习 Python 爬虫、数据清洗及数据库操作实践;中小团队,验证云开发环境在数据采集和存储方面的应用效率。1.3 案例时间预计总时长:约 50 分钟云开发环境配置:10 分钟GaussDB 数据库准备:10 分钟Python 爬虫编写与调试:20 分钟数据存入 GaussDB 与验证:10 分钟1.4 案例流程1.5 资源总览资源名称规格费用使用时长云开发环境鲲鹏通用计算增强型 kc1免费50分钟GaussDB 数据库4 vCPUs / 16G免费50分钟公网 EIP5Mbit/s0.8元/GB按流量计费Python库requests, bs4, psycopg2免费-二、案例准备工作2.1 配置云开发环境1.登录华为开发者空间;2.选择创建远程云开发环境;3.创建开发环境创建远程开发环境时需要填写以下信息:环境名称(必填):支持字母、数字和下划线,长度不超过 15 个字符,且不能以数字开头。开放端口(可选):默认为 22 端口,可额外开放 8080-8089 范围内的端口。默认账号(必填):默认为 developer,可自定义,需为 4~16 位小写字母,禁止使用系统保留用户名(如 root、agent)。密码(必填):需为 8~32 位字符,且至少包含 大写字母、小写字母、数字、特殊字符(~!@#$%^*-_=+?) 中的三种类型。4.将服务开机5.安装 CLI 工具包hdspace.exe 是一款面向开发者的命令行工具(CLI),用于在 PC 端对 开发者空间云开发环境 进行创建与管理。借助该工具,开发者不仅可以快速建立与云开发环境的隧道连接,还能实现远程操作,例如:上传/下载文件编写与编辑代码编译与执行程序下载方式在 Web 端进入已创建的云开发环境,点击 【远程连接】 → 【立即下载】,即可将 hdspace.exe 工具包下载到本地。6.配置环境变量以 Windows 11 为例,配置步骤如下:在任务栏搜索框输入 “环境变量”,或在 控制面板 → 系统和安全 → 系统 → 高级系统设置 中找到 环境变量 入口。在弹出的窗口中,找到 系统变量 下的 Path,点击 编辑。在 Path 配置界面中,点击 新建,输入 hdspace.exe 所在目录的路径。确认保存设置并退出。完成上述操作后,用户即可在 任意命令行窗口(如 CMD、PowerShell)中直接使用 hdspace.exe 命令。安装成功截图如下常用命令如下含义命令云开发环境顶层帮助hdspace devenv --help开发者空间配置帮助hdspace config --help开发者空间版本帮助hdspace version --help云开发环境列表帮助hdspace devenv list --help创建云开发环境帮助hdspace devenv create --help启动云开发环境帮助hdspace devenv start –help关闭云开发环境帮助hdspace devenv close --help删除云开发环境帮助hdspace devenv delete --help建立隧道帮助hdspace devenv start-tunnel --help7.获取并配置 AK/SK在使用 CLI 工具操作云开发环境前,需要完成 身份认证。这依赖于 AK(Access Key) 与 SK(Secret Key):AK(Access Key):访问密钥的唯一 ID,用于标识用户身份,通常可在网络传输中公开。SK(Secret Key):秘密密钥,仅用户与服务端持有,用于生成请求签名,确保请求合法性并防止未授权访问。配置 AK/SK 等价于在 CLI 工具中完成身份认证,从而使开发者能够通过命令行安全地管理和操作云开发环境。8.获取 AK/SK在下载的文件中可找到 AK/SK,格式类似如下:配置 AK/SK在命令行中执行以下命令:hdspace config根据提示依次输入 AK 和 SK:输入 AK 后回车输入 SK(注意:SK 输入时不会显示回显),并再次输入确认配置完成后,CLI 工具即可正常使用身份认证信息访问云开发环境。9.查看可用云开发环境信息2.2 领取GaussDB数据库目前可以免费领取GaussDB在线试用版(2025年 06月 21日 - 2025年 12月 31日)。当报名申请审核通过后,进入 开发者空间工作台。此时页面会1.显示 GaussDB 免费试用 提示,点击 立即开通 即可完成开通操作。若需远程访问数据库实例,必须为其绑定弹性公网 EIP(EIP 需单独购买,按需计费,价格约为 0.33 元/小时)。在控制台中,点击目标数据库实例名称,即可进入 GaussDB 基本信息 页面:2.登录GaussDB数据库回到云数据库控制台,登录数据库:填写密码后测试成功:3.成功登录页面如下4.点击绑定弹性公网IP,就可远程使用。三、爬取小说存入GaussDB数据库实战GaussDB是华为自主创新研发的分布式关系型数据库。该产品支持分布式事务,同城跨AZ部署,数据0丢失,支持1000+的扩展能力,PB级海量存储。同时拥有云上高可用,高可靠,高安全,弹性伸缩,一键部署,快速备份恢复,监控告警等关键能力,能为企业提供功能全面,稳定可靠,扩展性强,性能优越的企业级数据库服务。1.创建GaussD数据库-- 确保使用 public schema SET search_path TO public; -- 创建小说表 CREATE TABLE IF NOT EXISTS public.novel ( id VARCHAR(36) PRIMARY KEY, -- UUID 主键,由应用端生成 title VARCHAR(100) NOT NULL, -- 小说标题 content TEXT NOT NULL -- 小说正文 ); 2.python链接GaussD数据库# GaussDB 连接信息,请根据你的实际信息修改 GAUSSDB_CONFIG = { "host": "your_gaussdb_host", "port": 5432, "database": "your_db", "user": "your_user", "password": "your_password" } 3.核心代码def main(): try: # 连接数据库 conn = psycopg2.connect(**GAUSSDB_CONFIG) cursor = conn.cursor() print("成功连接 GaussDB!") # 创建小说表 cursor.execute(""" CREATE TABLE IF NOT EXISTS public.novel ( id VARCHAR(36) PRIMARY KEY, title VARCHAR(100) NOT NULL, content TEXT NOT NULL ); """) print("小说表创建成功(如果不存在)") # 插入数据 insert_query = "INSERT INTO public.novel (id, title, content) VALUES (%s, %s, %s) ON CONFLICT (id) DO NOTHING;" cursor.executemany(insert_query, novel_data) conn.commit() print("小说数据已成功存入数据库!") except Exception as e: print("操作失败:", e) finally: if cursor: cursor.close() if conn: conn.close() print("数据库连接已关闭。") if __name__ == "__main__": main() 4.爬虫主要代码# 给请求指定一个请求头来模拟chrome浏览器 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36'} server = 'XXXX' # 地址 book = 'XXX' # 定义DB mysql = MyPymysqlPool("dbMysql") # 获取章节内容 def get_contents(chapter): req = requests.get(url=chapter) html = req.content html_doc = str(html, 'gbk') bf = BeautifulSoup(html_doc, 'html.parser') texts = bf.find_all('div', id="content") # 获取div标签id属性content的内容 \xa0 是不间断空白符 content = texts[0].text.replace('\xa0' * 4, '\n') return content # 主方法 def main(): res = requests.get(book, headers=headers) html = res.content html_doc = str(html, 'gbk') # 使用自带的html.parser解析 soup = BeautifulSoup(html_doc, 'html.parser') # 获取所有的章节 a = soup.find('div', id='list').find_all('a') print('总章节数: %d ' % len(a)) for each in a: try: chapter = server + each.get('href') content = get_contents(chapter) chapter = each.string write_db(chapter, content) except Exception as e: print(e) mysql.dispose() if __name__ == '__main__': main() 5.GaussDB数据库情况。通过本次实战,我们完成了从模拟爬取小说数据到存入 GaussDB 数据库的完整流程。实践过程中,我们掌握了以下要点:GaussDB 的优势与使用支持分布式事务、PB级存储和高可用部署,保证数据安全与扩展能力。使用 public schema 创建表,合理设计主键(UUID)和字段类型,确保数据唯一性与完整性。Python 连接数据库与操作使用 psycopg2 连接 GaussDB,并通过事务控制确保数据一致性。利用 ON CONFLICT DO NOTHING 避免重复插入报错,提高脚本健壮性。爬虫模拟与数据处理模拟爬取章节内容并清洗 HTML 标签,实现章节正文的规范化存储。将每条小说数据生成 UUID 并插入数据库,实现与表结构一致的数据管理。实战心得通过模拟爬虫与数据库存储的结合,理解了爬取、清洗、存储数据的完整流程。对企业级数据库的设计与操作有了直观的认识,为后续大规模数据管理与分析奠定基础。总之,本次实战不仅验证了 GaussDB 的稳定性与高效性,也为小说数据爬取、清洗和存储提供了完整的流程示例,可作为类似场景的参考模板。我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 https://bbs.huaweicloud.com/forum/thread-0229189398343651003-1-1.html
-
基于远程开发环境部署Flask与开发者空间GaussDB的实践应用构建在线TODO清单(保姆级指南实战案例)案例介绍本案例将演示如何在华为开发者空间的远程云开发环境中部署 Flask 应用,并与 GaussDB 数据库 对接,实现一个基础的 TODO 清单 Web 应用。通过该实践,开发者能够掌握以下技能:在云开发环境中快速搭建 Flask 项目;配置并连接 GaussDB 数据库;使用 Flask ORM 完成数据库建表与数据操作;实现一个可运行的 TODO 清单应用。这种结合方式能够帮助开发者快速上手 Web 开发,并体验华为开发者空间提供的一站式开发环境。案例内容本案例将演示如何在华为开发者空间的远程云开发环境中部署 Flask 应用,并与 GaussDB 数据库 对接,实现一个基础的 TODO 清单 Web 应用。通过该实践,开发者能够掌握以下技能:在云开发环境中快速搭建 Flask 项目;配置并连接 GaussDB 数据库;使用 Flask ORM 完成数据库建表与数据操作;实现一个可运行的 TODO 清单应用。这种结合方式能够帮助开发者快速上手 Web 开发,并体验华为开发者空间提供的一站式开发环境。一、概述在现代 Web 应用开发中,快速迭代与云端部署 已成为常态。传统的本地开发环境虽然灵活,但在协作效率、资源配置以及数据库运维方面往往存在一定的局限。为了解决这些问题,越来越多的开发者开始借助 远程开发环境 与 云数据库服务 来完成应用构建。Flask 作为一个轻量化的 Python Web 框架,因其简单、灵活、可扩展的特性,广泛应用于原型验证和小型应用开发。而华为开发者空间所提供的 GaussDB 云数据库 与 云开发环境,为开发者提供了一体化的开发体验:无需繁琐配置,即可在云端完成代码编写、数据库对接和应用部署。本案例将通过构建一个 TODO 清单应用,演示如何在远程云开发环境中部署 Flask,并与 GaussDB 结合,实现一个完整的从环境搭建到应用落地的流程。通过该实践,开发者不仅能够熟悉 Flask 与数据库交互的核心方法,还能深入体验基于华为云生态的高效开发模式。1.1 案例介绍Flask 是一个轻量级 Python Web 框架,以简洁、灵活著称,非常适合快速构建小型应用或原型系统。本案例结合 Flask 与 GaussDB,通过远程开发环境完成从环境搭建到应用部署的全流程操作。最终效果:在浏览器访问 Flask 服务;可在页面中创建和查看 TODO 任务;数据持久化存储在 GaussDB 数据库中。1.2 适用对象中小企业希望快速验证产品原型;个人开发者尝试云端开发与数据库应用;高校学生学习 Flask 与数据库交互实践。1.3 案例时间预计总时长:约 40 分钟。1.4 案例流程1.在华为开发者空间申请云开发环境;2.配置 Flask 运行环境并安装依赖包;3.申请并绑定 GaussDB 数据库实例;4.在 Flask 中配置数据库连接;5。启动 Flask Web 服务并完成 TODO 清单应用测试。1.5 资源总览资源名称规格费用使用时长云开发环境鲲鹏通用计算增强型 kc1免费40分钟GaussDB 数据库4 vCPUs / 16G免费40分钟公网 EIP5Mbit/s0.8元/GB按流量计费二、案例准备工作2.1 配置云开发环境1.登录华为开发者空间;2.选择创建远程云开发环境;3.创建开发环境创建远程开发环境时需要填写以下信息:环境名称(必填):支持字母、数字和下划线,长度不超过 15 个字符,且不能以数字开头。开放端口(可选):默认为 22 端口,可额外开放 8080-8089 范围内的端口。默认账号(必填):默认为 developer,可自定义,需为 4~16 位小写字母,禁止使用系统保留用户名(如 root、agent)。密码(必填):需为 8~32 位字符,且至少包含 大写字母、小写字母、数字、特殊字符(~!@#$%^*-_=+?) 中的三种类型。4.将服务开机5.安装 CLI 工具包hdspace.exe 是一款面向开发者的命令行工具(CLI),用于在 PC 端对 开发者空间云开发环境 进行创建与管理。借助该工具,开发者不仅可以快速建立与云开发环境的隧道连接,还能实现远程操作,例如:上传/下载文件编写与编辑代码编译与执行程序下载方式在 Web 端进入已创建的云开发环境,点击 【远程连接】 → 【立即下载】,即可将 hdspace.exe 工具包下载到本地。6.配置环境变量以 Windows 11 为例,配置步骤如下:在任务栏搜索框输入 “环境变量”,或在 控制面板 → 系统和安全 → 系统 → 高级系统设置 中找到 环境变量 入口。在弹出的窗口中,找到 系统变量 下的 Path,点击 编辑。在 Path 配置界面中,点击 新建,输入 hdspace.exe 所在目录的路径。确认保存设置并退出。完成上述操作后,用户即可在 任意命令行窗口(如 CMD、PowerShell)中直接使用 hdspace.exe 命令。安装成功截图如下常用命令如下含义命令云开发环境顶层帮助hdspace devenv --help开发者空间配置帮助hdspace config --help开发者空间版本帮助hdspace version --help云开发环境列表帮助hdspace devenv list --help创建云开发环境帮助hdspace devenv create --help启动云开发环境帮助hdspace devenv start –help关闭云开发环境帮助hdspace devenv close --help删除云开发环境帮助hdspace devenv delete --help建立隧道帮助hdspace devenv start-tunnel --help7.获取并配置 AK/SK在使用 CLI 工具操作云开发环境前,需要完成 身份认证。这依赖于 AK(Access Key) 与 SK(Secret Key):AK(Access Key):访问密钥的唯一 ID,用于标识用户身份,通常可在网络传输中公开。SK(Secret Key):秘密密钥,仅用户与服务端持有,用于生成请求签名,确保请求合法性并防止未授权访问。配置 AK/SK 等价于在 CLI 工具中完成身份认证,从而使开发者能够通过命令行安全地管理和操作云开发环境。8.获取 AK/SK在下载的文件中可找到 AK/SK,格式类似如下:配置 AK/SK在命令行中执行以下命令:hdspace config根据提示依次输入 AK 和 SK:输入 AK 后回车输入 SK(注意:SK 输入时不会显示回显),并再次输入确认配置完成后,CLI 工具即可正常使用身份认证信息访问云开发环境。9.查看可用云开发环境信息2.2 领取GaussDB数据库目前可以免费领取GaussDB在线试用版(2025年 06月 21日 - 2025年 12月 31日)。当报名申请审核通过后,进入 开发者空间工作台。此时页面会1.显示 GaussDB 免费试用 提示,点击 立即开通 即可完成开通操作。若需远程访问数据库实例,必须为其绑定弹性公网 EIP(EIP 需单独购买,按需计费,价格约为 0.33 元/小时)。在控制台中,点击目标数据库实例名称,即可进入 GaussDB 基本信息 页面:2.登录GaussDB数据库回到云数据库控制台,登录数据库:填写密码后测试成功:3.成功登录页面如下4.点击绑定弹性公网IP,就可远程使用。三、在线TODO清单应用搭建3.1 项目背景功能分析这是一个基于Flask框架和GaussDB数据库构建的在线TODO清单网站,提供简洁美观的界面和完整的任务管理功能。功能特点📝 添加任务:创建新的待办事项,支持标题和详细描述✅ 完成任务:一键标记任务为已完成/未完成🗑️ 删除任务:轻松移除不需要的任务📋 任务列表:按创建时间倒序显示所有任务💬 消息提示:操作反馈及时显示📱 响应式设计:适配各种屏幕尺寸🎨 现代UI:采用Tailwind CSS构建的简洁美观界面项目结构GaussDB/ ├── app.py # 主应用程序文件 ├── requirements.txt # 项目依赖 ├── .env # 环境变量配置 ├── .gitignore # Git忽略规则 ├── templates/ # HTML模板目录 │ └── index.html # 主页面模板 └── README.md # 项目说明文档 3.2 GaussDB数据库样例数据如下: (1, '学习GaussDB基础', '了解GaussDB的基本概念和架构', FALSE), (2, '创建TODO应用', '使用Flask和GaussDB构建简单的TODO应用', FALSE), (3, '配置数据库连接', '确保应用能够正确连接到GaussDB', TRUE), (4, '测试SQL执行', '验证各种SQL语句在GaussDB中的执行情况', FALSE), (5, '解决权限问题', '修复创建表和序列时的权限错误', FALSE); 完整的GaussDB数据库执行命令如下:-- GaussDB schema 权限问题解决方案 -- 1. 检查当前连接使用的schema SELECT current_schema(); -- 2. 查看可用的schema列表 SELECT nspname FROM pg_namespace; -- 3. 尝试切换到public schema(如果有权限) SET search_path TO public; -- 4. 在public schema中创建todo表 CREATE TABLE public.todo ( id INT NOT NULL PRIMARY KEY, title VARCHAR(100) NOT NULL, description TEXT, completed BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- 5. 插入样例数据 INSERT INTO public.todo (id, title, description, completed) VALUES (1, '学习GaussDB基础', '了解GaussDB的基本概念和架构', FALSE), (2, '创建TODO应用', '使用Flask和GaussDB构建简单的TODO应用', FALSE), (3, '配置数据库连接', '确保应用能够正确连接到GaussDB', TRUE), (4, '测试SQL执行', '验证各种SQL语句在GaussDB中的执行情况', FALSE), (5, '解决权限问题', '修复创建表和序列时的权限错误', FALSE); -- 6. 验证插入结果 SELECT * FROM public.todo; -- 使用说明: -- 1. 执行此脚本前,请确保您有权限访问和使用public schema -- 2. 如果切换schema失败,请联系数据库管理员授予适当的权限 -- 3. 如果仍然无法创建表,可能需要管理员在特定schema中为您创建表结构 这份脚本主要用于解决 GaussDB 中 schema 权限不足 导致的建表问题。流程包括:首先检查当前连接所使用的 schema,并查看可用的 schema 列表;若有权限则切换到 public schema,在其中创建 todo 表并插入示例数据;最后通过查询验证插入结果。使用时需确保用户具备 public schema 的访问与操作权限,如仍遇到权限限制,则需联系数据库管理员授予相应权限或由管理员在目标 schema 下预建表结构。3.3 todo清单构建创建并使用一个简单的 TODO 清单表,以实现任务管理的基本功能。主要步骤包括:选择合适的 schema、创建表结构、插入示例数据以及验证数据。选择或切换 schema通过 current_schema() 查看当前连接的 schema,使用 SET search_path TO public; 切换到 public schema(前提是具有权限)。创建 TODO 表表结构设计包括 id(主键)、title、description、completed 状态、created_at 和 updated_at 时间戳等字段,支持记录任务详情及状态跟踪。插入示例数据向 todo 表中插入若干任务条目,用于后续的测试和展示。验证与查询通过 SELECT * FROM public.todo; 检查数据是否成功写入,并可根据 completed 状态或其他字段进行筛选和排序,实现基本的任务管理功能。这种方法不仅便于快速搭建实验性 TODO 应用,也为后续通过应用程序(如 Flask)对 GaussDB 的访问提供了数据基础。增加数据删除数据更新数据GaussDB数据库信息在本节中,我们通过 GaussDB 成功构建了一个简单的 TODO 清单表,实现了任务的增删改查操作。首先,确保使用具有权限的 schema(如 public),然后创建包含任务 ID、标题、描述、状态和时间戳等字段的表结构。接着,通过插入示例数据完成初始填充,并通过查询验证数据的正确性。同时展示了增、删、改操作的实际效果,使得用户能够直观了解 GaussDB 对任务数据的管理能力。整体流程为后续基于应用程序(如 Flask)构建完整的 TODO 应用打下了坚实的数据基础,也体现了 GaussDB 在日常开发中灵活、高效的数据库操作能力。四.心得通过本案例的实践,我们可以获得以下几点重要心得与经验:4.1 云开发环境的优势与体验快速搭建与即开即用华为开发者空间提供的云开发环境无需复杂的本地配置,只需创建环境并启动,即可开始编码与调试。这对于中小团队或个人开发者尤其方便,能够节省环境配置时间,并避免“环境不一致”问题。一体化的开发体验在同一平台下完成代码开发、数据库管理、依赖安装和远程调试,极大提升了开发效率。同时,CLI 工具 hdspace.exe 提供了命令行管理、文件上传下载、隧道连接等功能,使得远程操作更加灵活便捷。云端协作与可扩展性通过云开发环境,团队成员可以共享同一开发环境,实现代码与数据的一体化管理。环境资源可按需扩展,例如增加计算或存储能力,非常适合原型验证和小型项目开发。4.2 GaussDB 数据库的使用体会权限管理与 schema 问题GaussDB 默认 schema 权限可能会影响建表和数据操作。通过切换到 public schema 或联系管理员授予权限,可以顺利完成数据库建表、插入数据和查询操作。这提醒开发者在云数据库使用中,应充分了解权限体系与 schema 管理策略。数据操作便捷无论是通过 SQL 脚本插入样例数据,还是在 Flask 应用中进行增删改查,GaussDB 的执行效率和稳定性表现良好。在实际应用中,开发者可以快速验证数据操作,保证前端应用的响应速度和数据一致性。云数据库安全与远程访问绑定弹性公网 EIP 后可实现远程访问,结合 AK/SK 配置可确保操作安全。开发者在生产环境中应注意安全策略,例如限制 IP、启用访问控制和加密连接,确保数据安全。4.3 Flask 与数据库结合的实践经验轻量级快速开发Flask 框架以简洁著称,非常适合快速构建原型应用。在本案例中,通过少量代码就完成了 TODO 清单的页面展示、任务增删改查以及数据持久化,体现了 Flask 的灵活性。ORM 与数据库交互在实际开发中,可以使用 Flask 的 ORM(如 SQLAlchemy)进行数据库操作,使数据操作更加直观、可维护。通过本案例,我们掌握了基本的 ORM 映射与查询方法,为后续复杂应用打下基础。前后端分离与响应式设计本案例采用 Tailwind CSS 构建界面,简洁美观且自适应不同设备。结合 Flask 提供的数据接口,可进一步扩展为前后端分离的 Web 应用,实现更丰富的功能。4.4 总结与经验分享小型应用原型验证本案例演示了如何在 40 分钟内完成一个完整的 TODO 清单应用,从环境搭建到数据库对接,再到应用部署,充分体现了云开发环境的高效性和可操作性。注意权限与配置在使用云数据库时,应提前了解 schema 权限、AK/SK 配置及公网访问策略,避免在开发过程中因权限问题导致操作受限。可拓展性与实践价值本案例可以作为其他小型 Web 应用的模板,例如博客系统、在线笔记、任务管理工具等。开发者可在此基础上进一步扩展功能,例如用户登录、任务分类、提醒机制等。云开发与本地开发结合尽管云开发环境方便快捷,但在处理大规模数据或复杂计算时,本地开发环境仍有其优势。建议开发者根据项目需求灵活选择开发方式,云端和本地相结合,以提高开发效率。通过本案例,开发者不仅掌握了 Flask 与 GaussDB 的基础使用方法,也积累了远程云开发环境实践经验,为后续更复杂 Web 应用开发奠定了坚实基础。我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 https://bbs.huaweicloud.com/forum/thread-0229189398343651003-1-1.html
-
案例介绍本文基于华为云开发者空间所提供的远程开发环境基础配合 Spring Boot + MyBatis-Plus + GaussDB(PostgreSQL 兼容)实现“单库共享表”的行级多租户 SaaS 原型,覆盖认证授权(JWT)、租户隔离、审计留痕与 API 文档联动,提供可复制的一键演示与脚本化验收路径。案例内容一、概述1.1 案例介绍本案例基于华为云开发者空间与 GaussDB(PostgreSQL 兼容),使用 Spring Boot + MyBatis-Plus 实现“单库共享表”的行级多租户 SaaS 原型。核心能力包括:多租户隔离:tenant_id 行级隔离,MyBatis-Plus TenantLine 插件透明生效认证授权:JWT + Spring Security,登录发放令牌,校验 X-Tenant-Id 与 token 中 tid 一致性API 文档:springdoc + Knife4j,一键 Authorize 后可在文档内直接调试审计留痕:记录租户/用户/URI/状态/耗时,便于排错与审计演示与验收:saas-demo.html 支持 60 秒完成登录→切租→数据对比→权限验证,并提供 curl 脚本复现交付物包含可运行源码、DDL/演示数据、环境变量化配置与“流程演示/结果确认”图文,适合评审快速验证与二次复用。1.2 适用对象在华为云上快速搭建多租户后端原型的工程师/学生/创业团队评估 GaussDB(PostgreSQL 兼容)与 Spring 生态组合可行性的团队需要标准化认证授权、API 文档联动与审计能力的 B 端业务研发教学/培训场景中演示多租户与云上数据库最佳实践的讲师与助教前置要求:具备 Java/Spring Boot 与基础 SQL 知识;本地或云开发环境具备 JDK 11、Maven,GaussDB 已开通并放行访问;按文档设置 DB_URL/DB_USERNAME/DB_PASSWORD/JWT_SECRET 等环境变量即可运行。1.3 案例时间本案例总时长预计100分钟。1.4 案例流程1.5 资源总览本案例预计花费0.8元。资源名称规格单价(元)时长(分钟)华为开发者空间 - 云开发环境鲲鹏通用计算增强型 kc1 | 2vCPUs | 4G | HCE2.0免费45华为开发者空间 - 生态版GaussDB单副本集中式版 | 4 vCPUs | 16G | HCE OS 64bit (200GB)免费30弹性公网IP按流量计费 5Mbit/s0.8元/GB25二、GaussDB云数据库领取与配置2.1 领取免费版GaussDB免费报名领取GaussDB在线试用版(2025年06月 21日 - 2025年 12月 31日),有1000个名额,数量有限,链接如下:免费领取GaussDB在线试用版(1-3工作日内报名审核通过后会有短信通知)(1)报名申请审核通过后,进入开发者空间工作台,会有GaussDB免费试用提示,点击立即开通即可。(2)填写GaussDB数据库开通参数:虚拟私有云:如下图到控制台创建,采用默认参数即可。安全组:采用默认安全组。管理员密码:自定义设置,保存好后续使用。(3)设置完后点击开通,开通成功后如下图提示:(4)开通成功后,点击立即前往进入GaussDB数据库实例页面,等待状态切换为"正常",如下图所示:(5)(可选)绑定弹性公网EIP,如果需要远程操作数据库必须要绑定公网EIP(EIP需购买,按流量计费0.8元/GB,以控制台为准)点击数据库实例名称,进入GaussDB的基本信息界面:点击绑定弹性公网IP,若开发者没有购买,则需要购买弹性公网IP。购买时选择按需计费,购买成功后截图如下:回到数据库基本信息界面,再次绑定公网IP,绑定成功截图如下:2.2 登录GaussDB数据库回到云数据库控制台,登录数据库:点击登录,再点击同意并继续:输入密码,点击测试连接,再次点击登录,即可登录成功。2.2.1 新建数据库登录成功后,点击新建数据库:输入数据库名称 saas_demo 点击 确定 按钮,数据库创建成功:2.2.2 新建表点击 SQL查询Schema选择 public ,执行以下SQL语句,创建表:-- 多租户 SaaS 系统核心表结构(GaussDB/PostgreSQL 兼容) -- 执行顺序:自上而下 -- 1. 租户表 CREATE TABLE IF NOT EXISTS t_tenant ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, code VARCHAR(64) NOT NULL UNIQUE, status SMALLINT NOT NULL DEFAULT 1, -- 0=禁用 1=正常 2=过期 expire_time TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- 2. 用户表 CREATE TABLE IF NOT EXISTS t_user ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, username VARCHAR(64) NOT NULL, password_hash VARCHAR(256) NOT NULL, email VARCHAR(128), mobile VARCHAR(32), status SMALLINT NOT NULL DEFAULT 1, -- 0=禁用 1=正常 last_login TIMESTAMPTZ, deleted SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_user_tenant UNIQUE (tenant_id, username), CONSTRAINT fk_user_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ); CREATE INDEX idx_user_tenant ON t_user (tenant_id); -- 3. 角色表 CREATE TABLE IF NOT EXISTS t_role ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL, description VARCHAR(256), built_in SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_role_name UNIQUE (tenant_id, name), CONSTRAINT fk_role_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ); CREATE INDEX idx_role_tenant ON t_role (tenant_id); -- 4. 权限 / 菜单表 CREATE TABLE IF NOT EXISTS t_permission ( id BIGSERIAL PRIMARY KEY, name VARCHAR(128) NOT NULL, code VARCHAR(128) NOT NULL UNIQUE, type VARCHAR(16) NOT NULL, -- api/menu/button path VARCHAR(256), method VARCHAR(8), parent_id BIGINT, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- 5. 关联表:用户-角色 CREATE TABLE IF NOT EXISTS t_user_role ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_user_role UNIQUE (user_id, role_id), CONSTRAINT fk_ur_user FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE, CONSTRAINT fk_ur_role FOREIGN KEY (role_id) REFERENCES t_role(id) ON DELETE CASCADE ); -- 6. 关联表:角色-权限 CREATE TABLE IF NOT EXISTS t_role_permission ( id BIGSERIAL PRIMARY KEY, role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_role_perm UNIQUE (role_id, permission_id), CONSTRAINT fk_rp_role FOREIGN KEY (role_id) REFERENCES t_role(id) ON DELETE CASCADE, CONSTRAINT fk_rp_perm FOREIGN KEY (permission_id) REFERENCES t_permission(id) ON DELETE CASCADE ); -- 7. 业务:分类 CREATE TABLE IF NOT EXISTS t_category ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_cat_name UNIQUE (tenant_id, name), CONSTRAINT fk_cat_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ); CREATE INDEX idx_cat_tenant ON t_category (tenant_id); -- 8. 业务:商品 CREATE TABLE IF NOT EXISTS t_product ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, category_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, price NUMERIC(10,2) NOT NULL, stock INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_prod_name UNIQUE (tenant_id, name), CONSTRAINT fk_prod_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id), CONSTRAINT fk_prod_cat FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE INDEX idx_prod_tenant ON t_product (tenant_id); -- 9. 审计日志 CREATE TABLE IF NOT EXISTS t_audit_log ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, user_id BIGINT, action VARCHAR(256) NOT NULL, request_uri VARCHAR(256), ip VARCHAR(45), ua VARCHAR(256), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_log_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id), CONSTRAINT fk_log_user FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE INDEX idx_log_tenant_time ON t_audit_log (tenant_id, created_at); 写入测试数据-- 更真实的多租户演示数据(GaussDB 兼容,幂等) -- 采用 NOT EXISTS 以便重复执行不会产生重复数据 -- 1) 租户 INSERT INTO t_tenant (id, name, code, status, created_at, updated_at) SELECT 1, '演示租户A', 'demoA', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_tenant WHERE code='demoA'); INSERT INTO t_tenant (id, name, code, status, created_at, updated_at) SELECT 2, '演示租户B', 'demoB', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_tenant WHERE code='demoB'); -- 2) 全局权限(API 示例) INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '分类查询', 'category:read', 'api', '/categories', 'GET', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='category:read'); INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '分类写入', 'category:write', 'api', '/categories', 'POST', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='category:write'); INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '商品查询', 'product:read', 'api', '/products', 'GET', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='product:read'); INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '商品写入', 'product:write', 'api', '/products', 'POST', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='product:write'); INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '用户查询', 'user:read', 'api', '/users', 'GET', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='user:read'); INSERT INTO t_permission (name, code, type, path, method, parent_id, created_at, updated_at) SELECT '用户写入', 'user:write', 'api', '/users', 'POST', NULL, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_permission WHERE code='user:write'); -- 3) 角色(admin / editor / viewer) INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 1, 'admin', '管理员(全权限)', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=1 AND name='admin'); INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 1, 'editor', '编辑(商品/分类读写)', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=1 AND name='editor'); INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 1, 'viewer', '只读', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=1 AND name='viewer'); INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 2, 'admin', '管理员(全权限)', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=2 AND name='admin'); INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 2, 'editor', '编辑(商品/分类读写)', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=2 AND name='editor'); INSERT INTO t_role (tenant_id, name, description, built_in, created_at, updated_at) SELECT 2, 'viewer', '只读', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_role WHERE tenant_id=2 AND name='viewer'); -- 4) 用户(示例口令仍为占位明文,首次登录后将自动迁移为 BCrypt) INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 1, 'alice', 'hashA', 'alice@example.com', '13800000001', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=1 AND username='alice'); INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 1, 'dave', 'hashD', 'dave@example.com', '13800000003', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=1 AND username='dave'); INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 1, 'eve', 'hashE', 'eve@example.com', '13800000002', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=1 AND username='eve'); INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 2, 'bob', 'hashB', 'bob@example.com', '13900000001', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=2 AND username='bob'); INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 2, 'carol', 'hashC', 'carol@example.com', '13900000003', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=2 AND username='carol'); INSERT INTO t_user (tenant_id, username, password_hash, email, mobile, status, created_at, updated_at) SELECT 2, 'mallory', 'hashM', 'mallory@example.com', '13900000002', 1, now(), now() WHERE NOT EXISTS (SELECT 1 FROM t_user WHERE tenant_id=2 AND username='mallory'); -- 5) 分类(每租户 5 个) INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 1, 'A-饮料', 1, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=1 AND name='A-饮料'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 1, 'A-零食', 2, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=1 AND name='A-零食'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 1, 'A-生鲜', 3, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=1 AND name='A-生鲜'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 1, 'A-个护', 4, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=1 AND name='A-个护'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 1, 'A-家居', 5, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=1 AND name='A-家居'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 2, 'B-饮料', 1, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=2 AND name='B-饮料'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 2, 'B-零食', 2, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=2 AND name='B-零食'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 2, 'B-数码', 3, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=2 AND name='B-数码'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 2, 'B-美妆', 4, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=2 AND name='B-美妆'); INSERT INTO t_category (tenant_id, name, sort_order, created_at) SELECT 2, 'B-服饰', 5, now() WHERE NOT EXISTS (SELECT 1 FROM t_category WHERE tenant_id=2 AND name='B-服饰'); -- 6) 商品(每分类 1~2 个示例) -- 租户1 INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 1, c.id, 'A-可乐330ml', 3.50, 200, now() FROM t_category c WHERE c.tenant_id=1 AND c.name='A-饮料' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=1 AND p.name='A-可乐330ml'); INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 1, c.id, 'A-薯片80g', 5.90, 120, now() FROM t_category c WHERE c.tenant_id=1 AND c.name='A-零食' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=1 AND p.name='A-薯片80g'); INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 1, c.id, 'A-生菜500g', 4.20, 60, now() FROM t_category c WHERE c.tenant_id=1 AND c.name='A-生鲜' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=1 AND p.name='A-生菜500g'); -- 租户2 INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 2, c.id, 'B-气泡水500ml', 4.50, 150, now() FROM t_category c WHERE c.tenant_id=2 AND c.name='B-饮料' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=2 AND p.name='B-气泡水500ml'); INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 2, c.id, 'B-巧克力100g', 8.80, 80, now() FROM t_category c WHERE c.tenant_id=2 AND c.name='B-零食' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=2 AND p.name='B-巧克力100g'); INSERT INTO t_product (tenant_id, category_id, name, price, stock, created_at) SELECT 2, c.id, 'B-蓝牙耳机', 199.00, 35, now() FROM t_category c WHERE c.tenant_id=2 AND c.name='B-数码' AND NOT EXISTS (SELECT 1 FROM t_product p WHERE p.tenant_id=2 AND p.name='B-蓝牙耳机'); -- 7) 角色-权限绑定 -- admin = 所有权限 INSERT INTO t_role_permission (role_id, permission_id, created_at) SELECT r.id, p.id, now() FROM t_role r CROSS JOIN t_permission p WHERE r.name='admin' AND NOT EXISTS (SELECT 1 FROM t_role_permission rp WHERE rp.role_id=r.id AND rp.permission_id=p.id); -- editor = category/product 读写 + user 读 INSERT INTO t_role_permission (role_id, permission_id, created_at) SELECT r.id, p.id, now() FROM t_role r JOIN t_permission p ON p.code IN ('category:read','category:write','product:read','product:write','user:read') WHERE r.name='editor' AND NOT EXISTS (SELECT 1 FROM t_role_permission rp WHERE rp.role_id=r.id AND rp.permission_id=p.id); -- viewer = 只读(category/product/user 读) INSERT INTO t_role_permission (role_id, permission_id, created_at) SELECT r.id, p.id, now() FROM t_role r JOIN t_permission p ON p.code IN ('category:read','product:read','user:read') WHERE r.name='viewer' AND NOT EXISTS (SELECT 1 FROM t_role_permission rp WHERE rp.role_id=r.id AND rp.permission_id=p.id); -- 8) 用户-角色绑定 -- 租户1:alice=admin, dave=editor, eve=viewer INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='admin' WHERE u.tenant_id=1 AND u.username='alice' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='editor' WHERE u.tenant_id=1 AND u.username='dave' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='viewer' WHERE u.tenant_id=1 AND u.username='eve' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); -- 租户2:bob=admin, carol=editor, mallory=viewer INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='admin' WHERE u.tenant_id=2 AND u.username='bob' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='editor' WHERE u.tenant_id=2 AND u.username='carol' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); INSERT INTO t_user_role (user_id, role_id, created_at) SELECT u.id, r.id, now() FROM t_user u JOIN t_role r ON r.tenant_id=u.tenant_id AND r.name='viewer' WHERE u.tenant_id=2 AND u.username='mallory' AND NOT EXISTS (SELECT 1 FROM t_user_role ur WHERE ur.user_id=u.id AND ur.role_id=r.id); -- 9) 审计样例 INSERT INTO t_audit_log (tenant_id, user_id, action, request_uri, ip, ua, created_at) SELECT 1, (SELECT id FROM t_user WHERE tenant_id=1 AND username='alice'), 'seed view users', '/users', '127.0.0.1', 'seed', now() WHERE NOT EXISTS (SELECT 1 FROM t_audit_log WHERE tenant_id=1 AND action='seed view users'); INSERT INTO t_audit_log (tenant_id, user_id, action, request_uri, ip, ua, created_at) SELECT 2, (SELECT id FROM t_user WHERE tenant_id=2 AND username='bob'), 'seed view categories', '/categories', '127.0.0.1', 'seed', now() WHERE NOT EXISTS (SELECT 1 FROM t_audit_log WHERE tenant_id=2 AND action='seed view categories'); 2.2.3 公网捆绑(可选)绑定弹性公网EIP,如果需要远程操作数据库必须要绑定公网EIP(EIP需购买,按流量计费0.8元/GB,以控制台为准)点击数据库实例名称,进入GaussDB的基本信息界面:点击绑定弹性公网IP,若开发者没有购买,则需要购买弹性公网IP。购买时选择按需计费,购买成功后截图如下:2.2.4 安全组放行选中我们的 GaussDB 实例找到我们捆绑的安全组和端口(默认为8000)放行 8000 端口 以实例实际端口为准(控制台可见)三、项目构建3.1 开发者空间配置面向广大开发者群体,华为开发者空间提供一个随时访问的“开发平台”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。开发者可以登录华为云账号后可以直接进入华为开发者空间开发平台界面,点击云开发环境即可进入云开发环境页面。注:初次进入云开发环境需签署协议,勾选协议后,点击【进入】。云开发环境如下图所示:输入 开发环境名称 开放端口 用户 和密码等相关参数点击 确定3.2 PC端通过cli工具连接云开发环境本案例中,使用华为云《开发者空间云开发环境使用指导》的“三. PC端环境配置”、“四. 本地PC端操作云开发环境中的1.开机、2.建立隧道连接”章节完成cli工具安装、环境配置、创建云开发环境、开机、建立隧道连接的功能。3.3 项目建立3.4 git 仓库代码项目地址:https://gitcode.com/weixin_52568491/saas_demo相关详细代码 可以参考gitcode仓库左上角 文件 打开 找到我们刚刚git下来的项目3.5 修改相关变量以下变量可通过环境变量设置,或直接在 application.yml 中修改为你的数据库与安全配置:变量说明DB_URLJDBC URL(以实例实际端口为准)DB_USERNAME数据库用户名DB_PASSWORD数据库密码JWT_SECRET至少 32 字节的随机秘钥JWT_TTL_MILLISJWT 有效期(毫秒,默认 3600000)DOCS_PUBLIC_ENABLED / ALLOW_PLAINTEXT_PASSWORDS / HEADER_TENANT_FILTER_ENABLED演示/生产开关,见下方建议设置 DB_URL: jdbc:postgresql://<EIP或内网IP>:<端口>/saas_demo?sslmode=disable&charSet=UTF8设置 DB_USERNAME: <数据库用户名>设置 DB_PASSWORD: <数据库密码>设置 JWT_SECRET: ≥32 字符的随机秘钥可选 JWT_TTL_MILLIS: 3600000(单位毫秒,默认 1 小时)演示期:DOCS_PUBLIC_ENABLED=true、ALLOW_PLAINTEXT_PASSWORDS=true生产期:DOCS_PUBLIC_ENABLED=false、ALLOW_PLAINTEXT_PASSWORDS=false始终:HEADER_TENANT_FILTER_ENABLED=false(仅在需要从请求头解析租户时临时开启)注意JWT_SECRET 建议 ≥32 字节,否则应用可能无法启动。端口以 GaussDB 实例实际端口为准(控制台可见)。修改变量后需重启应用使配置生效。3.6 运行代码然后点击右上角 运行按钮 出现 8080 则视为运行成功3.7 演示页穿透这里再进行一个隧道的链接hdspace devenv start-tunnel --instance-id=instance-id --remote-port=8080 --local-port=8080 这样我们就能在本地看到 8080端口的运行情况了四、流程演示目标:用最少步骤验证 “多租户隔离 + 认证授权 + 文档联动 + 可视化演示” 四个核心能力。受众收益:按本文操作,60 秒内完成登录、切换租户、拉取数据与对比。4.1 入口地址演示页(推荐先看):http://localhost:8080/saas-demo.htmlSwagger UI:http://localhost:8080/swagger-ui.htmlKnife4j:http://localhost:8080/doc.html(受 docs.public-enabled 控制)OpenAPI JSON:http://localhost:8080/v3/api-docs说明:部署到云开发环境后,将 localhost 替换为服务器的公网域名/IP 与端口(本案例就采用隧道的概念 采用localhost即可)。4.2 前置检查应用已启动:控制台出现“Tomcat started on port 8080”已设置环境变量:DB_URL、DB_USERNAME、DB_PASSWORD、JWT_SECRET、JWT_TTL_MILLIS演示/生产开关(建议值)演示:DOCS_PUBLIC_ENABLED=true,ALLOW_PLAINTEXT_PASSWORDS=true(如有明文存量)生产:DOCS_PUBLIC_ENABLED=false,ALLOW_PLAINTEXT_PASSWORDS=false,HEADER_TENANT_FILTER_ENABLED=false4.3 一分钟极速演示(纯前端)打开演示页 saas-demo.html登录:租户ID=1,用户名=alice,输入密码并登录,页面会显示 Bearer <token>点击“查询分类(租户1)”,可见 A 租户的分类列表修改“租户ID=2”,再次查询分类,可见 B 租户的分类(与租户1不同)打开“用户-角色/角色-权限”查询按钮,查看同一用户在不同租户下的权限差异验收点:切换租户ID后,同一接口数据不同,证明行级隔离生效。4.4 文档联动演示(Swagger/Knife4j)进入文档页,调用 /auth/login 获取 token点击 Authorize,粘贴 Bearer <token>在全局参数里添加 X-Tenant-Id(1 或 2)依次调用:GET /categories:查看分类列表(租户隔离)GET /users/{userId}/roles:查看用户在租户下的角色GET /roles/{roleId}/permissions:查看角色权限切换 X-Tenant-Id 再次调用,对比差异说明:未设置全局 X-Tenant-Id 时默认取 token 的 tid。4.5 脚本复现(可用于 CI 与评审机)登录获取 Tokencurl -s -X POST "http://localhost:8080/auth/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "tenantId=1&username=alice&password=<你的密码>" # 从返回 JSON 中取出 token 字段 查询分类(租户1)TOKEN=<上一步的token> curl -s -H "Authorization: Bearer $TOKEN" -H "X-Tenant-Id: 1" \ http://localhost:8080/categories查询分类(租户2,对比)curl -s -H "Authorization: Bearer $TOKEN" -H "X-Tenant-Id: 2" \ http://localhost:8080/categories验收点:租户1/2 的分类列表不同。4.6 常见问题速查403 Forbidden:未携带 Authorization: Bearer <token>,或 Token 过期 → 重新登录获取401 Unauthorized:X-Tenant-Id 与 Token 中 tid 不一致 → 保持一致或仅用 Token 的 tid2999 数据库操作失败:检查 DB_URL/账号/密码 与安全组/白名单、初始化表结构是否完整文档为空/打不开:演示时设 DOCS_PUBLIC_ENABLED=true(生产请关闭)登录成功但明文口令:系统会自动迁移为 BCrypt,后续建议统一改为 BCrypt 存储4.7 安全与最佳实践生产关闭匿名文档:DOCS_PUBLIC_ENABLED=false禁用明文兼容:ALLOW_PLAINTEXT_PASSWORDS=false(已提供自动迁移为 BCrypt)仅保留 Token 驱动的租户解析,默认关闭 HEADER_TENANT_FILTER_ENABLEDJWT_SECRET 使用高强度随机值;数据库凭据、URL 来自环境变量4.8 结果确认登录成功(演示页)说明:在 saas-demo.html 输入租户ID/用户名/密码,点击登录,页面展示 Bearer <token>。期望:返回 200,页面出现 token,后续按钮可用。分类查询(租户1)说明:在演示页保持租户ID=1,点击“查询分类(租户1)”。期望:返回 A 租户的分类列表。切换租户后分类查询(租户2)说明:将演示页租户ID切换为 2,重复查询分类。期望:返回 B 租户的数据,与租户1明显不同(数量或名称不同)。用户-角色/角色-权限联查(演示页按钮)说明:在演示页分别点击“查询用户角色”“查询角色权限”。期望:能展示当前租户下用户绑定的角色,以及角色对应的权限清单。文档授权后直接调试(Swagger/Knife4j)说明:全局 Authorization,粘贴 Bearer <token>,并设置 X-Tenant-Id;调用 /roles。期望:文档内调试返回 200 且与演示页一致。审计日志留痕说明:展示一条典型请求的日志记录(含租户、URI、状态与耗时)。期望:能看到形如“REQ GET/POST 200 (…)ms”的记录,tenantId 正确。五、项目总结及优化方案5.1 安全加固口令与认证统一使用 BCrypt 存储口令;已实现“首次登录自动迁移为 BCrypt”,建议在演示后批量迁移与回灌校验。生产禁止明文口令兼容:ALLOW_PLAINTEXT_PASSWORDS=false。JWT 秘钥通过环境变量注入:JWT_SECRET;设置定期轮换策略与最小长度(≥32 字节)。接口与文档生产关闭匿名文档:DOCS_PUBLIC_ENABLED=false;仅在演示/开发打开。白名单仅保留:/auth/login、文档与静态资源(视需要);其他均需鉴权。多租户来源统一默认仅以 Token 中 tid 为准;当请求头存在 X-Tenant-Id 时需与 token 一致,否则 401。禁用头部租户注入(生产):HEADER_TENANT_FILTER_ENABLED=false。5.2 多租户与权限登录查询避免重复条件两种做法二选一:在登录/刷新方法开始前 TenantContext.setTenantId(tenantId);使用 @InterceptorIgnore(tenantLine = "true") 并保留手动 tenantId 条件。公共表访问规范已忽略租户拦截:t_permission、t_role_permission、t_user_role、t_tenant、t_audit_log。明确仅允许读或受控写,防止越权写入;关键写入操作加入审计。权限粒度保持 API/菜单/按钮三级;补充角色模板(admin/editor/viewer)与“跨租户复制模板”能力(后续迭代)。5.3 性能与稳定性索引与查询高频 where 建议复合索引:(tenant_id, created_at)、(tenant_id, username)、(tenant_id, name) 等。避免 N+1:列表页尽量批量查询或使用视图;分页设定上限(如 size<=100)。连接池与超时Hikari:合理设置 maximumPoolSize/idleTimeout;数据库连接/查询/读写超时分档。迁移与版本引入 Flyway/Liquibase 管控 DDL/DML,替代手工执行,保证环境一致性与可回滚。5.4 可观测性与审计审计日志规范字段:请求ID、tenantId、userId、URI、方法、状态码、耗时、IP、UA、时间戳、失败原因(可选)。数据保留周期与归档策略(冷热分层)。运行指标登录成功率、401/403 比例、慢查询 TopN、连接池耗尽告警;接入 Prometheus/Grafana 展示。5.5 交付与运维构建分环境Maven Profile:dev/staging/prod,对文档与明文兼容等开关进行差异化控制。部署与回滚Docker 镜像 + 一键部署脚本;健康检查/优雅停机;灰度发布与快速回滚策略。配置管理环境变量/密钥管理(如 KMS/SM);敏感信息不落库不落盘。5.6 测试与质量集成测试使用 Testcontainers 连接 GaussDB/PostgreSQL 兼容容器,覆盖租户隔离、权限边界与越权用例。冒烟/回归脚本化“登录→切租→分类对比→角色权限→文档联动”全链路;纳入 CI。5.7 成本与可用性容量规划GaussDB 规格与最大连接数评估;按照 QPS 与数据量扩容;读写分离/只读副本(按产品形态选)。备份与恢复设定 RPO/RTO 目标并演练恢复流程;关键表 point-in-time 恢复验证。5.8 后续演进多租户形态抽象从“单库共享表”演进到“单库多 Schema/多库多实例”,提供迁移脚本与双写验证方案。安全强化限流与防刷、二次认证、密码强度与生命周期策略;租户级安全策略配置中心。业务扩展模块化能力模板(用户/角色/权限/分类/商品),支持租户级启停与配额。我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 https://bbs.huaweicloud.com/forum/thread-0229189398343651003-1-1.html
-
余年方十七,习编程已有数载,虽非初学,然深知学无止境,技艺之精进,如逆水行舟,不进则退。 技艺渐精 昔日初学时,常为类型错误而苦恼,今已能熟练运用多种语言。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)
-
帖子标题作者案例中心地址【案例共创】基于华为开发者空间开发平台 MCP资产快速构建税务AI助手服务小草飞上天cid:link_0【案例共创】使用华为云开发者空间AI agent功能零基础开发一个购房助手CC07cid:link_1【案例共创】基于华为开发者空间构建实时股票分析助手miyaliancid:link_2【案例共创】使用华为云开发者空间 AI Agent 进行昇腾C算子开发知识库构建黄生cid:link_3【案例共创】基于华为开发者空间开发平台构建旅游规划助手yd_272483742cid:link_4【案例共创】使用开发者空间 AI Agent+RAG+高德地图MCP开发班车出行助手神一样的老师cid:link_5【案例共创】基于华为开发者空间使用 FastAPI 构建 MCP 天气查询服务胡琦cid:link_6【案例共创】基于华为云开发者空间+Flexus+Dify平台的AI Agent构建实战:搭建大学报考志愿建议助手柠檬味拥抱cid:link_7【案例共创】基于华为开发者空间构建本地MCP Server + DeepSeek + Cherry Studio实现股市分析助手yd_269585276cid:link_8【案例共创】基于华为云开发者空间的AI Agent [旅行灵感生成器]智能体柠檬味拥抱cid:link_9【案例共创】基于仓颉 + DeepSeek + MCP 的智能膳食分析助手给无眠点压力cid:link_10【案例共创】使用 HarmonyOS NEXT和Mass快速开发NutPITalk坚果派开发者cid:link_11【案例共创】基于开发者空间使用Kotaemon开源RAG UI和华为云Maas搭建本地/私人AI知识库云聪明cid:link_12【案例共创】Mass满血DeepSeek服务结合云主机搭建私有化数学家Agent小草飞上天cid:link_13【案例共创】基于华为云鲲鹏服务器和HCE OS部署热门大模型对话网页云聪明cid:link_14【案例共创】基于模型平台ModelArts Studio和开源Agent框架Dify.AI构建聊天助手实践胡琦cid:link_15【案例共创】在开发者空间使用 MateChat 和Mass快速开发智能对话界面胡琦cid:link_16【案例共创】基于Maas服务及maxkb专业打造企业级私有化智能知识库小草飞上天cid:link_17【案例共创】使用MaxKB接入华为Maas服务快速打造AI问答Agent鸢尾离夏cid:link_18
-
本次产品体验官活动获奖名单如下:活动名称礼品名称中奖用户社区昵称活动一:体验打卡开发者定制双肩包xw9520hw_008613456511577_01开发者定制短袖T恤hw71567953hw005378643gridinfoxiexinlian0318GT-weixin_56237756hw_008618840962406_01hid_clf-2oizpt7skaqwangshuang188hid_xyga9qwo7mo6jkmhid_tqv_kyl8l_0ane5活动二:实践互动华为手环9(NFC款)wangshuang188wyz13694166286HDC定制大礼包banjinyd_295732424yd_256610494开发者定制短袖T恤yd_225198557小强鼓掌yd_210430557yd_213866132yd_239827768yd_279183797guokuiyongyd_257913329yd_283391333yd_292408942yd_253770151yd_280344472zhenggxypr189yd_233412103恭喜以上中奖用户,请于10月30日前通过本链接反馈中奖收件信息,感谢大家对华为开发者空间的支持! 华为开发者空间权益大放送,每周畅领100万商用级Tokens!基于CloudMatrix384超节点,Token时延低、TPM/RPM速率高,助您开发商用级AI应用! 📆 活动时间即日起—9月30日 📌 活动流程戳我领取代金券→0元购DeepSeek-R1 / V3-64K 100万tokens→报名活动 → 参与活动 → 获得激励 🎁 活动方式活动一:体验打卡体验打卡开发者空间云开发环境功能,参与开发者定制礼品抽奖参与方式:进入开发者空间,体验空间云开发环境新能力奖项设置:开发者定制双肩包*2个,开发者定制短袖T恤*10件 活动二:实践互动完成开发者空间“开发平台”新功能案例体验,抽华为手环等好礼参与方式:选择下方任意一个或多个案例或独立完成与“云开发桌面/环境”相关的案例进行体验,并在评论区分享案例截图(也欢迎书写你对案例实践后的体验和感受),活动结束后,将在评论区用户中抽奖。体验功能案例名称难度系数云开发桌面基于DeepSeek和Dify构建心理咨询师应用★★【案例共创】在开发者空间开发MCP Server完成对GaussDB数据库的操作★★★★基于仓颉编程语言+DeepSeek实现智能聊天助手开发秘籍★★★开发者空间部署Cherry Studio+高德地图MCP Server构建出行规划助手★★★基于DeepSeek+Dify构建财务报表分析模型★★★云开发环境基于华为开发者空间云开发环境部署Coze Studio + Maas构建智能体应用★★★★基于开发者空间云开发环境,使用MateChat+MaaS构建智能对话应用★★★基于Cursor连接华为开发者空间-云开发环境,部署Dify+MaaS构建智能问答助手★★★★奖项设置:华为手环9(NFC款)*2个,HDC定制大礼包*3个(内含HDC双肩包、渔夫帽、小风扇及短袖T恤),开发者定制短袖T恤*15件中奖小妙招:完成多个案例或者独自开发完成新案例,会增加中奖概率!完成后请在回帖中注明完成了几个案例,以及案例名称。小编会对案例完成情况进行复核,请大家真实完成哦! 部分礼品示意PS:以上两个活动均可参与,且奖励可叠加。在使用中如有产品优化建议,可参与开发者空间产品体验官活动,更多礼品等你解锁~ 📝 活动规则1、抽奖方式:活动结束后,我们将从参与活动的用户中(华为云新老用户均可参与),通过巨公平台或Excel 函数形式抽取获奖用户。获奖名单将在活动结束后的7个工作日内公布。2、活动二仅限于在“华为开发者空间”内体验相关案例项目,其他项目建议不参与此次活动,否则视为无效内容,具体参考案例中心内容。另外活动将根据有效评论数设置获奖人员数量,规则如下:有效评论数量获奖名额1052010302040以上全部开启3、本次活动将根据实际参与情况发放奖励,包括但不限于用户百分之百中奖或奖项轮空的情况。 💡 活动说明用户限制说明:1、参加本次社区活动的用户必须为华为云注册用户。同时为保证活动公平性,禁止用户以IAM账号身份参与活动,否则将视为无效。2、领取奖品的用户需为华为云实名用户,未完成实名认证的用户将不发放活动奖励。3、本次活动如一个实名认证对应多个账号,只有一个账号可领取奖励。如在同一概率活动中,同一账号重复获奖,只发放首先获奖奖品。4、本次活动一个实名认证账号只能对应一个收件人,如同一账号填写多个不同收件人,不予发放奖励。5、请开发者不要在活动期间随意修改社区昵称和华为云账号,由此产生的统计问题,如过了申诉期,小助手不再处理。(申诉期为活动结果公示3天内。)奖品发放说明:1、本活动结束之后10个工作日内公示获奖信息,获奖开发者用户需在截止时间在获奖信息收集表中填写获奖信息,获奖信息截止收集日过后10个工作日内,将统一发出奖品。华为云遵守《中华人民共和国个人信息保护法》规定,将以上个人信息仅用于礼品发放之目的,不会向任何第三方披露。若由于获奖开发者用户自身原因(包括但不限于联系方式有误、身份不符或超过截止登记日期等)造成奖品无法发送,视为获奖开发者用户放弃领奖。2、为保证活动的公平公正,华为云有权对恶意刷活动资源(“恶意”是指为获取资源而异常注册账号等破坏活动公平性的行为),利用资源从事违法违规行为的开发者用户收回抽奖及奖励资格。3、若发放奖品时,出现库存不足,则优先发放等价值的其他实物奖品;HDC限定礼包为24/25年款随机发货;4、所有参加本活动的开发者用户,均视为认可并同意遵守《华为云开发者用户协议》,包括以援引方式纳入《华为云开发者用户协议》、《可接受的使用政策》、《法律声明》、《隐私政策声明》、相关服务等级协议(SLA),以及华为云服务网站规定的其他协议和政策(统称为“云服务协议”)的约束。5、如果您不同意本活动规则和云服务协议的条款,请勿参加本活动。
-
华为云开发者空间☁️昇腾NPU实现AI工业质检本案例将在华为云开发者空间工作台⚙️AI Notebook 中使用免费的🎉 昇腾 NPU 910B ✨完成YOLO11模型训练,利用SAHI切片辅助超推理框架实现PCB缺陷检测。1. 下载模型和数据集📦首先在Notebook的代码块中粘贴并运行下面的代码,下载解压本案例所需的训练数据和模型文件:import os import zipfile if not os.path.exists('yolo11_train_ascend.zip'): os.system('wget -q https://orangepi-ascend.obs.cn-north-4.myhuaweicloud.com/yolo11_train_ascend.zip') if not os.path.exists('yolo11_train_ascend'): zip_file = zipfile.ZipFile('yolo11_train_ascend.zip') zip_file.extractall() zip_file.close() 2. 安装依赖包🛠️安装YOLO11所需的依赖包以及SAHI库,构建项目的运行环境:!pip install ultralytics==8.3.160 ultralytics-thop==2.0.14 sahi==0.11.26 numpy==1.26.4 Defaulting to user installation because normal site-packages is not writeable Looking in indexes: https://mirrors.huaweicloud.com/repository/pypi/simple Collecting ultralytics==8.3.160 Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/7b/8d/924524ff26c0ed0ba43b90cc598887e2b06f3bf00dd51a505a754ecb138d/ultralytics-8.3.160-py3-none-any.whl (1.0 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.0/1.0 MB 9.8 MB/s eta 0:00:00 Collecting ultralytics-thop==2.0.14 Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/a6/10/251f036b4c5d77249f9a119cc89dafe8745dc1ad1f1a5f06b6a3988ca454/ultralytics_thop-2.0.14-py3-none-any.whl (26 kB) Collecting sahi==0.11.26 Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/5e/8a/9782c8088af52e6f41fee59c77b5117783c0d6eafde45c96ca3912ec197f/sahi-0.11.26-py3-none-any.whl (115 kB) Collecting numpy==1.26.4 Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (14.2 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.2/14.2 MB 42.9 MB/s eta 0:00:00 0:00:01 Requirement already satisfied: matplotlib>=3.3.0 in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (3.10.0) Requirement already satisfied: opencv-python>=4.6.0 in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (4.10.0.84) Requirement already satisfied: pillow>=7.1.2 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (11.0.0) Requirement already satisfied: pyyaml>=5.3.1 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (6.0.2) Requirement already satisfied: requests>=2.23.0 in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (2.32.3) Requirement already satisfied: scipy>=1.4.1 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (1.14.1) Requirement already satisfied: torch>=1.8.0 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (2.1.0) Requirement already satisfied: torchvision>=0.9.0 in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (0.16.0) Requirement already satisfied: tqdm>=4.64.0 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (4.67.1) Requirement already satisfied: psutil in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (5.9.8) Requirement already satisfied: py-cpuinfo in /home/service/.local/lib/python3.10/site-packages (from ultralytics==8.3.160) (9.0.0) Requirement already satisfied: pandas>=1.1.4 in /usr/local/python3.10/lib/python3.10/site-packages (from ultralytics==8.3.160) (2.2.3) Requirement already satisfied: click in /usr/local/python3.10/lib/python3.10/site-packages (from sahi==0.11.26) (8.1.8) Collecting fire (from sahi==0.11.26) Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz (87 kB) Installing build dependencies ... done Getting requirements to build wheel ... done Preparing metadata (pyproject.toml) ... done Collecting pybboxes==0.1.6 (from sahi==0.11.26) Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/3c/3f/46f6613b41a3c2b4f7af3b526035771ca5bb12d6fdf3b23145899f785e36/pybboxes-0.1.6-py3-none-any.whl (24 kB) Collecting shapely>=2.0.0 (from sahi==0.11.26) Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/29/51/0b158a261df94e33505eadfe737db9531f346dfa60850945ad25fd4162f1/shapely-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.9 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.9/2.9 MB 22.0 MB/s eta 0:00:00 Collecting terminaltables (from sahi==0.11.26) Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/c4/fb/ea621e0a19733e01fe4005d46087d383693c0f4a8f824b47d8d4122c87e0/terminaltables-3.1.10-py2.py3-none-any.whl (15 kB) Requirement already satisfied: contourpy>=1.0.1 in /home/service/.local/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (1.3.1) Requirement already satisfied: cycler>=0.10 in /home/service/.local/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /home/service/.local/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (4.55.3) Requirement already satisfied: kiwisolver>=1.3.1 in /home/service/.local/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (1.4.8) Requirement already satisfied: packaging>=20.0 in /usr/local/python3.10/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (24.2) Requirement already satisfied: pyparsing>=2.3.1 in /home/service/.local/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (3.2.0) Requirement already satisfied: python-dateutil>=2.7 in /usr/local/python3.10/lib/python3.10/site-packages (from matplotlib>=3.3.0->ultralytics==8.3.160) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/python3.10/lib/python3.10/site-packages (from pandas>=1.1.4->ultralytics==8.3.160) (2024.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/python3.10/lib/python3.10/site-packages (from pandas>=1.1.4->ultralytics==8.3.160) (2024.2) Requirement already satisfied: charset-normalizer<4,>=2 in /home/service/.local/lib/python3.10/site-packages (from requests>=2.23.0->ultralytics==8.3.160) (3.4.1) Requirement already satisfied: idna<4,>=2.5 in /usr/local/python3.10/lib/python3.10/site-packages (from requests>=2.23.0->ultralytics==8.3.160) (3.10) Requirement already satisfied: urllib3<3,>=1.21.1 in /home/service/.local/lib/python3.10/site-packages (from requests>=2.23.0->ultralytics==8.3.160) (2.3.0) Requirement already satisfied: certifi>=2017.4.17 in /home/service/.local/lib/python3.10/site-packages (from requests>=2.23.0->ultralytics==8.3.160) (2024.12.14) Requirement already satisfied: filelock in /usr/local/python3.10/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (3.16.1) Requirement already satisfied: typing-extensions in /usr/local/python3.10/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (4.12.2) Requirement already satisfied: sympy in /usr/local/python3.10/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (1.13.3) Requirement already satisfied: networkx in /usr/local/python3.10/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (3.4.2) Requirement already satisfied: jinja2 in /usr/local/python3.10/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (3.1.5) Requirement already satisfied: fsspec in /home/service/.local/lib/python3.10/site-packages (from torch>=1.8.0->ultralytics==8.3.160) (2024.9.0) Collecting termcolor (from fire->sahi==0.11.26) Downloading https://mirrors.huaweicloud.com/repository/pypi/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl (7.7 kB) Requirement already satisfied: six>=1.5 in /usr/local/python3.10/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib>=3.3.0->ultralytics==8.3.160) (1.16.0) Requirement already satisfied: MarkupSafe>=2.0 in /home/service/.local/lib/python3.10/site-packages (from jinja2->torch>=1.8.0->ultralytics==8.3.160) (3.0.2) Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/python3.10/lib/python3.10/site-packages (from sympy->torch>=1.8.0->ultralytics==8.3.160) (1.3.0) Building wheels for collected packages: fire Building wheel for fire (pyproject.toml) ... done Created wheel for fire: filename=fire-0.7.0-py3-none-any.whl size=114330 sha256=a1f27d511635da524f8f51fa2d35ae22862e400cec55285acbc05ced6ef91371 Stored in directory: /home/service/.cache/pip/wheels/9b/dc/c7/06491fe82713723ab64494dbcfd521bdbe80cf26b5fcb5f564 Successfully built fire Installing collected packages: terminaltables, termcolor, numpy, shapely, pybboxes, fire, ultralytics-thop, sahi, ultralytics Attempting uninstall: numpy Found existing installation: numpy 1.24.4 Uninstalling numpy-1.24.4: Successfully uninstalled numpy-1.24.4 WARNING: The script f2py is installed in '/home/service/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. WARNING: The script sahi is installed in '/home/service/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. WARNING: The scripts ultralytics and yolo are installed in '/home/service/.local/bin' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts. gradio 5.9.1 requires markupsafe~=2.0, but you have markupsafe 3.0.2 which is incompatible. openmind 0.9.1 requires datasets<=2.21.0,>=2.18.0, but you have datasets 3.2.0 which is incompatible. openmind 0.9.1 requires openmind-hub==0.9.0, but you have openmind-hub 0.9.1 which is incompatible. openmind-datasets 0.7.1 requires datasets==2.18.0, but you have datasets 3.2.0 which is incompatible. openmind-evaluate 0.7.0 requires datasets==2.18.0, but you have datasets 3.2.0 which is incompatible. Successfully installed fire-0.7.0 numpy-1.26.4 pybboxes-0.1.6 sahi-0.11.26 shapely-2.1.1 termcolor-3.1.0 terminaltables-3.1.10 ultralytics-8.3.160 ultralytics-thop-2.0.14 [notice] A new release of pip is available: 24.3.1 -> 25.1.1 [notice] To update, run: pip install --upgrade pip3. 修改配置文件📝我们在配置文件中指定数据集路径和类别等信息,用于后续模型的训练:%%writefile yolo11_train_ascend/pcb.yaml # Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..] path: /opt/huawei/edu-apaas/src/init/yolo11_train_ascend/pcb_sliced # dataset root dir (absolute path) train: images/train # train images (relative to 'path') val: images/val # val images (relative to 'path') test: # test images (optional) # Classes,类别 names: 0: mouse_bite 1: open_circuit 2: short 3: spur 4: spurious_copperWriting yolo11_train_ascend/pcb.yaml4. 下载 Arial.ttf 字体🖋️为了避免影响训练进展,可以先提前下载字体文件并拷贝到 /home/service/.config/Ultralytics 路径下。!wget https://orangepi-ascend.obs.cn-north-4.myhuaweicloud.com/Arial.ttf !mkdir -p /home/service/.config/Ultralytics !cp Arial.ttf /home/service/.config/Ultralytics/Arial.ttf--2025-06-28 05:55:59-- https://pcb-sahi-public.obs.cn-southwest-2.myhuaweicloud.com/Arial.ttf Resolving pcb-sahi-public.obs.cn-southwest-2.myhuaweicloud.com (pcb-sahi-public.obs.cn-southwest-2.myhuaweicloud.com)... 100.125.6.3, 100.125.7.3, 100.125.6.131 Connecting to pcb-sahi-public.obs.cn-southwest-2.myhuaweicloud.com (pcb-sahi-public.obs.cn-southwest-2.myhuaweicloud.com)|100.125.6.3|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 773236 (755K) [application/x-font-ttf] Saving to: 'Arial.ttf' Arial.ttf 100%[===================>] 755.11K --.-KB/s in 0.004s 2025-06-28 05:55:59 (188 MB/s) - 'Arial.ttf' saved [773236/773236] 5. 模型训练🧠🔥我们使用yolo11n.pt预训练模型,利用昇腾NPU进行模型加速,设置模型的训练次数为10轮、图像的大小为640x640、开启8个数据加载线程每次送入模型32张图像进行迭代优化。%cd yolo11_train_ascend import torch import torch_npu from torch_npu.contrib import transfer_to_npu from ultralytics import YOLO # Load a model model = YOLO('yolo11n.pt') # load a pretrained model (recommended for training) # Train the model results = model.train(data='pcb.yaml', epochs=10, imgsz=640, workers=8, batch=32) %cd .. /home/service/.local/lib/python3.10/site-packages/IPython/core/magics/osm.py:417: UserWarning: This is now an optional IPython functionality, setting dhist requires you to install the `pickleshare` library. self.shell.db['dhist'] = compress_dhist(dhist)[-100:] /opt/huawei/edu-apaas/src/init/yolo11_train_ascend /home/service/.local/lib/python3.10/site-packages/torch_npu/utils/path_manager.py:82: UserWarning: Warning: The /usr/local/Ascend/ascend-toolkit/latest owner does not match the current user. warnings.warn(f"Warning: The {path} owner does not match the current user.") /home/service/.local/lib/python3.10/site-packages/torch_npu/utils/path_manager.py:82: UserWarning: Warning: The /usr/local/Ascend/ascend-toolkit/8.0.RC3/aarch64-linux/ascend_toolkit_install.info owner does not match the current user. warnings.warn(f"Warning: The {path} owner does not match the current user.") /home/service/.local/lib/python3.10/site-packages/torch_npu/contrib/transfer_to_npu.py:301: ImportWarning: ************************************************************************************************************* The torch.Tensor.cuda and torch.nn.Module.cuda are replaced with torch.Tensor.npu and torch.nn.Module.npu now.. The torch.cuda.DoubleTensor is replaced with torch.npu.FloatTensor cause the double type is not supported now.. The backend in torch.distributed.init_process_group set to hccl now.. The torch.cuda.* and torch.cuda.amp.* are replaced with torch.npu.* and torch.npu.amp.* now.. The device parameters have been replaced with npu in the function below: torch.logspace, torch.randint, torch.hann_window, torch.rand, torch.full_like, torch.ones_like, torch.rand_like, torch.randperm, torch.arange, torch.frombuffer, torch.normal, torch._empty_per_channel_affine_quantized, torch.empty_strided, torch.empty_like, torch.scalar_tensor, torch.tril_indices, torch.bartlett_window, torch.ones, torch.sparse_coo_tensor, torch.randn, torch.kaiser_window, torch.tensor, torch.triu_indices, torch.as_tensor, torch.zeros, torch.randint_like, torch.full, torch.eye, torch._sparse_csr_tensor_unsafe, torch.empty, torch._sparse_coo_tensor_unsafe, torch.blackman_window, torch.zeros_like, torch.range, torch.sparse_csr_tensor, torch.randn_like, torch.from_file, torch._cudnn_init_dropout_state, torch._empty_affine_quantized, torch.linspace, torch.hamming_window, torch.empty_quantized, torch._pin_memory, torch.autocast, torch.load, torch.Generator, torch.Tensor.new_empty, torch.Tensor.new_empty_strided, torch.Tensor.new_full, torch.Tensor.new_ones, torch.Tensor.new_tensor, torch.Tensor.new_zeros, torch.Tensor.to, torch.nn.Module.to, torch.nn.Module.to_empty ************************************************************************************************************* warnings.warn(msg, ImportWarning) /home/service/.local/lib/python3.10/site-packages/torch_npu/contrib/transfer_to_npu.py:260: RuntimeWarning: torch.jit.script and torch.jit.script_method will be disabled by transfer_to_npu, which currently does not support them, if you need to enable them, please do not use transfer_to_npu. warnings.warn(msg, RuntimeWarning) Creating new Ultralytics Settings v0.0.6 file ✅ View Ultralytics Settings with 'yolo settings' or at '/home/service/.config/Ultralytics/settings.json' Update Settings with 'yolo settings key=value', i.e. 'yolo settings runs_dir=path/to/dir'. For help see https://docs.ultralytics.com/quickstart/#ultralytics-settings. [W compiler_depend.ts:623] Warning: expandable_segments currently defaults to false. You can enable this feature by `export PYTORCH_NPU_ALLOC_CONF = expandable_segments:True`. (function operator()) Ultralytics 8.3.160 🚀 Python-3.10.15 torch-2.1.0 CUDA:0 (Ascend910B3, 62432MiB) engine/trainer: agnostic_nms=False, amp=True, augment=False, auto_augment=randaugment, batch=32, bgr=0.0, box=7.5, cache=False, cfg=None, classes=None, close_mosaic=10, cls=0.5, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=pcb.yaml, degrees=0.0, deterministic=True, device=None, dfl=1.5, dnn=False, dropout=0.0, dynamic=False, embed=None, epochs=10, erasing=0.4, exist_ok=False, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, half=False, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=640, int8=False, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolo11n.pt, momentum=0.937, mosaic=1.0, multi_scale=False, name=train, nbs=64, nms=False, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=None, rect=False, resume=False, retina_masks=False, save=True, save_conf=False, save_crop=False, save_dir=runs/detect/train, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=botsort.yaml, translate=0.1, val=True, verbose=True, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=8, workspace=None Overriding model.yaml nc=80 with nc=5 from n params module arguments 0 -1 1 464 ultralytics.nn.modules.conv.Conv [3, 16, 3, 2] 1 -1 1 4672 ultralytics.nn.modules.conv.Conv [16, 32, 3, 2] 2 -1 1 6640 ultralytics.nn.modules.block.C3k2 [32, 64, 1, False, 0.25] 3 -1 1 36992 ultralytics.nn.modules.conv.Conv [64, 64, 3, 2] 4 -1 1 26080 ultralytics.nn.modules.block.C3k2 [64, 128, 1, False, 0.25] 5 -1 1 147712 ultralytics.nn.modules.conv.Conv [128, 128, 3, 2] 6 -1 1 87040 ultralytics.nn.modules.block.C3k2 [128, 128, 1, True] 7 -1 1 295424 ultralytics.nn.modules.conv.Conv [128, 256, 3, 2] 8 -1 1 346112 ultralytics.nn.modules.block.C3k2 [256, 256, 1, True] 9 -1 1 164608 ultralytics.nn.modules.block.SPPF [256, 256, 5] 10 -1 1 249728 ultralytics.nn.modules.block.C2PSA [256, 256, 1] 11 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest'] 12 [-1, 6] 1 0 ultralytics.nn.modules.conv.Concat [1] 13 -1 1 111296 ultralytics.nn.modules.block.C3k2 [384, 128, 1, False] 14 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest'] 15 [-1, 4] 1 0 ultralytics.nn.modules.conv.Concat [1] 16 -1 1 32096 ultralytics.nn.modules.block.C3k2 [256, 64, 1, False] 17 -1 1 36992 ultralytics.nn.modules.conv.Conv [64, 64, 3, 2] 18 [-1, 13] 1 0 ultralytics.nn.modules.conv.Concat [1] 19 -1 1 86720 ultralytics.nn.modules.block.C3k2 [192, 128, 1, False] 20 -1 1 147712 ultralytics.nn.modules.conv.Conv [128, 128, 3, 2] 21 [-1, 10] 1 0 ultralytics.nn.modules.conv.Concat [1] 22 -1 1 378880 ultralytics.nn.modules.block.C3k2 [384, 256, 1, True] 23 [16, 19, 22] 1 431647 ultralytics.nn.modules.head.Detect [5, [64, 128, 256]] /home/service/.local/lib/python3.10/site-packages/torch_npu/utils/storage.py:38: UserWarning: TypedStorage is deprecated. It will be removed in the future and UntypedStorage will be the only storage class. This should only matter to you if you are using storages directly. To access UntypedStorage directly, use tensor.untyped_storage() instead of tensor.storage() if self.device.type != 'cpu': YOLO11n summary: 181 layers, 2,590,815 parameters, 2,590,799 gradients, 6.4 GFLOPs Transferred 448/499 items from pretrained weights Freezing layer 'model.23.dfl.conv.weight' AMP: running Automatic Mixed Precision (AMP) checks... [W compiler_depend.ts:51] Warning: CAUTION: The operator 'torchvision::nms' is not currently supported on the NPU backend and will fall back to run on the CPU. This may have performance implications. (function npu_cpu_fallback) AMP: checks passed ✅ train: Fast image access ✅ (ping: 0.0±0.0 ms, read: 620.7±42.3 MB/s, size: 454.2 KB) train: Scanning /opt/huawei/edu-apaas/src/init/yolo11_train_ascend/pcb_sliced/labels/train... 4646 images, 0 backgrounds, 0 corrupt: 100%|██████████| 4646/4646 [00:05<00:00, 848.20it/s] train: New cache created: /opt/huawei/edu-apaas/src/init/yolo11_train_ascend/pcb_sliced/labels/train.cache val: Fast image access ✅ (ping: 0.0±0.0 ms, read: 471.4±135.5 MB/s, size: 448.2 KB) val: Scanning /opt/huawei/edu-apaas/src/init/yolo11_train_ascend/pcb_sliced/labels/val... 422 images, 0 backgrounds, 0 corrupt: 100%|██████████| 422/422 [00:00<00:00, 520.44it/s] val: New cache created: /opt/huawei/edu-apaas/src/init/yolo11_train_ascend/pcb_sliced/labels/val.cache Plotting labels to runs/detect/train/labels.jpg... optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... optimizer: AdamW(lr=0.001111, momentum=0.9) with parameter groups 81 weight(decay=0.0), 88 weight(decay=0.0005), 87 bias(decay=0.0) Image sizes 640 train, 640 val Using 8 dataloader workers Logging results to runs/detect/train Starting training for 10 epochs... Closing dataloader mosaic Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 0%| | 0/146 [00:00<?, ?it/s] . /home/service/.local/lib/python3.10/site-packages/ultralytics/utils/tal.py:274: UserWarning: AutoNonVariableTypeMode is deprecated and will be removed in 1.10 release. For kernel implementations please use AutoDispatchBelowADInplaceOrView instead, If you are looking for a user facing API to enable running your inference-only workload, please use c10::InferenceMode. Using AutoDispatchBelowADInplaceOrView in user code is under risk of producing silent wrong result in some edge cases. See Note [AutoDispatchBelowAutograd] for more details. (Triggered internally at build/CMakeFiles/torch_npu.dir/compiler_depend.ts:74.) target_scores = torch.where(fg_scores_mask > 0, target_scores, 0) [W compiler_depend.ts:103] Warning: Non finite check and unscale on NPU device! (function operator()) 1/10 7.77G 2.238 5.333 1.761 8 640: 100%|██████████| 146/146 [01:31<00:00, 1.60it/s] Class Images Instances Box(P R mAP50 mAP50-95): 0%| | 0/7 [00:00<?, ?it/s] ..... Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:58<00:00, 8.39s/it] all 422 604 0.39 0.0656 0.0888 0.023 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 2/10 8.2G 1.876 2.724 1.462 8 640: 100%|██████████| 146/146 [01:16<00:00, 1.92it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.58it/s] all 422 604 0.451 0.238 0.214 0.0639 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 3/10 8.2G 1.825 1.912 1.445 8 640: 100%|██████████| 146/146 [01:12<00:00, 2.00it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.64it/s] all 422 604 0.339 0.291 0.244 0.0742 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 4/10 8.2G 1.748 1.571 1.398 4 640: 100%|██████████| 146/146 [01:12<00:00, 2.02it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.58it/s] all 422 604 0.409 0.361 0.335 0.117 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 5/10 8.2G 1.703 1.343 1.372 6 640: 100%|██████████| 146/146 [01:11<00:00, 2.05it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.66it/s] all 422 604 0.442 0.34 0.321 0.118 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 6/10 8.2G 1.673 1.26 1.343 5 640: 100%|██████████| 146/146 [01:11<00:00, 2.03it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.62it/s] all 422 604 0.605 0.49 0.53 0.224 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 7/10 8.2G 1.614 1.145 1.316 6 640: 100%|██████████| 146/146 [01:12<00:00, 2.00it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.58it/s] all 422 604 0.595 0.542 0.525 0.206 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 8/10 8.2G 1.578 1.067 1.294 7 640: 100%|██████████| 146/146 [01:11<00:00, 2.03it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.73it/s] all 422 604 0.754 0.629 0.685 0.307 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 9/10 8.21G 1.551 1.009 1.275 8 640: 100%|██████████| 146/146 [01:11<00:00, 2.04it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:04<00:00, 1.57it/s] all 422 604 0.782 0.618 0.703 0.315 Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 10/10 8.21G 1.5 0.9621 1.255 6 640: 100%|██████████| 146/146 [01:12<00:00, 2.02it/s] Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:03<00:00, 1.83it/s] all 422 604 0.8 0.661 0.732 0.354 10 epochs completed in 0.236 hours. Optimizer stripped from runs/detect/train/weights/last.pt, 5.5MB Optimizer stripped from runs/detect/train/weights/best.pt, 5.5MB Validating runs/detect/train/weights/best.pt... Ultralytics 8.3.160 🚀 Python-3.10.15 torch-2.1.0 CUDA:0 (Ascend910B3, 62432MiB) YOLO11n summary (fused): 100 layers, 2,583,127 parameters, 0 gradients, 6.3 GFLOPs ... Class Images Instances Box(P R mAP50 mAP50-95): 0%| | 0/7 [00:00<?, ?it/s] . Class Images Instances Box(P R mAP50 mAP50-95): 100%|██████████| 7/7 [00:07<00:00, 1.14s/it] all 422 604 0.799 0.663 0.732 0.355 mouse_bite 107 169 0.806 0.785 0.829 0.4 open_circuit 73 101 0.656 0.471 0.492 0.219 short 69 87 0.889 0.54 0.701 0.314 spur 95 134 0.864 0.714 0.76 0.342 spurious_copper 95 113 0.782 0.805 0.88 0.5 Speed: 0.1ms preprocess, 8.3ms inference, 0.0ms loss, 2.5ms postprocess per image Results saved to runs/detect/train /opt/huawei/edu-apaas/src/init /home/service/.local/lib/python3.10/site-packages/IPython/core/magics/osm.py:417: UserWarning: This is now an optional IPython functionality, setting dhist requires you to install the `pickleshare` library. self.shell.db['dhist'] = compress_dhist(dhist)[-100:] 模型训练好后,可以在runs/detect/train目录下查看训练结果,例如损失函数的变化曲线、mAP等评价指标📈💪。6. 图像切分检测✂️🔍最后我们利用SAHI框架对高清PCB图像进行切片推理,从而更精准地检测出PCB的瑕疵类别。import torch import torch_npu from torch_npu.contrib import transfer_to_npu from sahi.predict import get_sliced_prediction from sahi import AutoDetectionModel from PIL import Image detection_model = AutoDetectionModel.from_pretrained( model_type = 'ultralytics', model_path = "yolo11_train_ascend/runs/detect/train/weights/best.pt", confidence_threshold = 0.4, device = "cuda:0" ) 这里我们使用滑窗检测🔍的技术,将原始图像切分成640x640大小的子图🖼️,同时设置一定的重叠度,再分别预测每张子图,最后将所有的检测结果进行合并处理🛠️。image_path = "https://orangepi-ascend.obs.cn-north-4.myhuaweicloud.com/001.bmp" result = get_sliced_prediction( image_path, detection_model, slice_height = 640, slice_width = 640, overlap_height_ratio = 0.1, overlap_width_ratio = 0.1, perform_standard_pred = False, postprocess_class_agnostic = True, postprocess_match_threshold = 0.1, ) result.export_visuals(export_dir="output/", file_name="sliced_result") Image.open("output/sliced_result.png") Performing prediction on 24 slices.可以看到,模型准确无误的预测出PCB缺陷的位置、类别和置信度😄7. 小结📌本案例借助华为云开发者空间💡昇腾910B NPU完成YOLO11模型训练与PCB缺陷检测,并且结合SAHI实现高效切片推理🚀,华为云开发者空间💻AI Notebook开箱即用,大家快来体验吧!🤗 ----转自博客:https://bbs.huaweicloud.com/blogs/455280
-
一、 背景Qwen2-VL-7B-Instruct是通义千问系列中的一款多模态大模型,具备强大得视觉与语言理解能力。它在保持较小体积的同时,提供了出色的视觉理解和语言生成能力,是当前中文多模态AI领域的优秀选择之一。华为开发者空间内置昇腾NPU资源,开发者每天共有两个小时的免费使用时长,本次为开发者带来基于华为开发者空间Notebook部署Qwen2-VL-Instruct模型进行图片理解全流程。二、环境配置及模型部署首先,浏览器进入魔塔社区,获取Qwen2-VL-7B-Instruct模型文件进入华为开发者空间Notebook,进行模型下载,打开终端输入下载模型命令:git clone https://www.modelscope.cn/Qwen/Qwen2.5-VL-7B-Instruct.git模型下载完毕后开始配置环境,随后安装需要的工具包pip install qwen-vl-utils必要安装包下载完毕后,进入模型文件下获取路径。点击左上角 “+” 启动一个代码页,将以下地址复制到代码行中进行工具包的安装。pip install --upgrade transformers peft diffusers accelerate将以下代码复制到代码行中,其中替换模型路径。import os import torch # 设置 NPU 内存优化 os.environ["PYTORCH_NPU_ALLOC_CONF"] = "expandable_segments:True" # 修复 torch.compiler if not hasattr(torch.compiler, 'is_compiling'): torch.compiler.is_compiling = lambda: False import torch_npu from modelscope import Qwen2VLForConditionalGeneration, AutoProcessor from qwen_vl_utils import process_vision_info # Step 1: 确认 NPU 可用 assert torch.npu.is_available(), "NPU not available" # Step 2: 加载模型(使用 bfloat16) model = Qwen2VLForConditionalGeneration.from_pretrained( "/opt/huawei/edu-apaas/src/init/model/Qwen2-VL-7B-Instruct", torch_dtype=torch.bfloat16, device_map=None, trust_remote_code=True ) model = model.eval() # 关闭训练模式 model = model.to("npu:0") # Step 3: 限制图像 token 数量(🔥 关键) min_pixels = 256 * 28 * 28 max_pixels = 1280 * 28 * 28 processor = AutoProcessor.from_pretrained( "/opt/huawei/edu-apaas/src/init/model/Qwen2-VL-7B-Instruct", trust_remote_code=True, use_fast=False, min_pixels=min_pixels, max_pixels=max_pixels ) # Step 4: 构造输入 messages = [ { "role": "user", "content": [ { "type": "image", "image": "/opt/huawei/edu-apaas/src/init/model/Qwen2-VL-7B-Instruct/fed651d4f97246c4_big.jpg", }, {"type": "text", "text": "Describe this image."}, ], } ] text = processor.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) image_inputs, video_inputs = process_vision_info(messages) inputs = processor( text=[text], images=image_inputs, videos=video_inputs, padding=True, return_tensors="pt", ) inputs = inputs.to("npu:0") # Step 5: 推理 with torch.no_grad(): generated_ids = model.generate(**inputs, max_new_tokens=128) # 解码 generated_ids_trimmed = [ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) ] output_text = processor.batch_decode( generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False ) print("Output:", output_text) 代码复制完毕后,再将需要进行理解的图片上传到Notebook中,并将代码中的图片路径进行替换。替换完毕后,运行,最后结果会将上传的图片进行解释。至此,Qwen2-VL-7B-Instruct模型部署完毕。
-
华为开发者空间推荐官第一期活动可兑奖名单如下,请获奖的小伙伴在11月4日前反馈收件信息(逾期反馈不再补发),报名用户积分统计明细请查看附件清单。华为云账号名hw0****3314huy****34hw3****dan****jikongzhongxinhid****7_ovlxw7pv8mjac****bithhid****1ifa25zm3vwohid****oebixpzcsk1tGT-****0161443hw0****3976hw8****549lic****qian1207hid****20xjdev_3p9phid****ez52-g4tnneahw_****15102517719_01hw5****jos****yhid****8jbsk12d0e78hid****_zbcinmpv181GT-****5655215CSD****_35655215hid****m9v6qo05banrhid****69lei-l9eypqhid****35zv6a1s9uv-hid****9_o479gzk8poCCI****luo****hid****xmbj5f41hwr3mav****kshwfre****yGT-****_zhang_zhanzhu****clin****hw8****062h_v****9802063Rob****iuyshw_****18844196063_01mql****16hid****p8u1_k84yq4zhw_****17671701081_01a18****60983hid****gqha3hgwtdirhw6****927 开发者空间推荐官第一期来啦~成功邀请好友报名/开通开发者空间/参与空间案例实践,可获得不同积分激励,累计最高可兑换千元开发者定制好礼+千元代金券,不仅有华为音箱、耳机、手环,更有开发者双肩包、冲锋衣、云宝盲盒等好礼,快叫上小伙伴一起来参加吧~ 【活动时间】2025年8月27日-9月30日 【活动流程】① 完成报名 → ② 分享专属推广链接 → ③ 邀请好友报名 / 开通开发者空间 / 参与空间案例实践 → ④ 积分兑换激励ps:报名成功后一定要从“分享有礼”获取自己专属推广链接哦~ 【案例参考】可以选择下方任意1个案例进行实践或独立完成开发者空间云主机相关的开发,实践完成后请在此帖下方评论,评论需包含案例名称+案例截图+使用感受,活动结束后,小编会对有真实使用记录的同学给与积分激励技术场景阶段实践案例难度系数AI 入门云主机调用DeepSeek实现代码自动生成★★本地部署DeepSeek构建RAG向量数据库★★★基于DeepSeek搭建Agent智能助手★★★进阶自主编程之Cline×DeepSeek的智能融合探索★★★基于TensorFlow的手写体识别★★★基于PyTorch的手写体识别★★★场景开发开发者空间部署Cherry Studio+高德地图MCP Server构建出行规划助手★★★基于华为开发者云主机部署FastGPT并构建知识库智能体★★★FastGPT问答系统实战:知识库检索×联网搜索★★★基于开发者空间通过调用MySQL MCP Server实现对外部数据源的获取等实操★★★软件开发入门基于云主机的CodeArts IDE运行Java电商项目★基于CodeArts Repo云仓库管理云主机代码★进阶基于CodeArts Build的项目容器化构建★★★场景开发CodeArts IDE调用API实现电商平台问答助手★★使用云主机CodeArts IDE进行远程JVM服务调试★★ 【积分规则说明】积分项目积分数积分发放说明活动报名1分/人邀请者和被邀请者参加活动报名,可各积1分开通开发者空间2分/人每新邀请一个好友新开通空间,则邀请者和被邀请者各积2分参与空间案例实践5分/人完成空间案例实践或独自完成空间应用构建,在论坛评论区回帖案例名称+使用截图,核实真实有效后可积5分/人 【礼品兑换说明】序号积分兑换规则可兑换礼品(云资源代金券和实物礼品可同时获得)可兑换数量兑换前提条件云资源代金券实物礼品价值110分>积分>1分20元云资源代金券开发者定制鼠标垫(大号)200推荐开通开发者空间数≥2或完成空间案例实践220分>积分≥10分30元云资源代金券50元实物礼品100推荐开通开发者空间数≥3或完成空间案例实践330分>积分≥20分50元云资源代金券100元实物礼品30440分>积分≥30分100元云资源代金券200元实物礼品15推荐开通开发者空间数≥5或完成空间案例实践560分>积分≥40分200元云资源代金券300元实物礼品5推荐开通开发者空间数≥8或完成空间案例实践680分>积分≥60分300元云资源代金券500元实物礼品27100分>积分≥80分500元云资源代金券800元实物礼品1推荐开通开发者空间数≥12或完成空间案例实践8积分>100分800元云资源代金券1000元实物礼品1推荐开通开发者空间数≥20或完成空间案例实践1、活动结束后,按积分高低排名依次兑换,部分礼品兑换数量有限,必须足额积分后且满足兑换前提条件下再填写兑换问卷,否则兑奖无效。若高兑换档位名额已空,则按积分高低自动往下占用低档位兑换名额;反之若高兑换名额无人兑换,则低兑换名额会自动扩充。2、实物礼品包括华为无线耳机、体脂称、手环9、云宝礼盒、开发者定制冲锋衣、定制短袖等,如遇缺货,将随机发货,部分礼品实物图如下~ 【活动说明】用户限制说明:1、参加本次社区活动的用户必须为华为云注册用户。同时为保证活动公平性,禁止用户以IAM账号身份参与活动,否则将视为无效。2、领取奖品的用户需为华为云实名用户,未完成实名认证的用户将不发放对应活动奖励。3、本次活动如一个实名认证对应多个账号,只有一个账号可领取奖励。如在同一概率活动中,同一账号重复获奖,只发放首先获奖奖品。4、本次活动一个实名认证账号只能对应一个收件人,如同一账号填写多个不同收件人,不予发放奖励。5、请开发者不要在活动期间随意修改社区昵称和华为云账号,由此产生的统计问题,如过了申诉期,小助手不再处理。(申诉期为活动结果公示3天内。)奖品发放说明:1、本活动结束之后15个工作日内公示获奖信息,获奖开发者用户需在截止时间在获奖信息收集表中填写获奖信息,获奖信息截止收集日过后10个工作日内,将统一发出实物奖品和云资源代金券。华为云遵守《中华人民共和国个人信息保护法》规定,将以上个人信息仅用于礼品发放之目的,不会向任何第三方披露。若由于获奖开发者用户自身原因(包括但不限于联系方式有误、身份不符或超过截止登记日期等)造成奖品无法发送,视为获奖开发者用户放弃领奖。2、为保证活动的公平公正,华为云有权对恶意刷活动资源(“恶意”是指为获取资源而异常注册账号等破坏活动公平性的行为),利用资源从事违法违规行为的开发者用户收回抽奖及奖励资格。3、若发放实物奖品时出现库存不足,则优先发放等价值的其他实物奖品;云资源券领取有效期为60天,领取后有效使用时间为1年内,逾期未使用不予补发4、所有参加本活动的开发者用户,均视为认可并同意遵守《华为云开发者用户协议》,包括以援引方式纳入《华为云开发者用户协议》、《可接受的使用政策》、《法律声明》、《隐私政策声明》、相关服务等级协议(SLA),以及华为云服务网站规定的其他协议和政策(统称为“云服务协议”)的约束。
-
云学堂最新活动:云学堂用户推荐官(开发者空间)活动—第1期—点此立即报名 【获奖公示—云学堂集证有礼·码力全开活动】一、获奖公示详情如下:1、【积分数据公示】:见本论坛贴附件12、【邀请好友报名公示】:见本论坛贴附件23、【积分兑好礼公示】:见本论坛贴附件3——务必查看4、【完成云实验抽奖公示】:见本论坛贴附件4——务必查看5、完成空间案例实操获奖名单公示,点此前往论坛贴查看!6、【微认证/开发者认证问题反馈有奖公示】:见本论坛贴附件5——务必查看(公示时间2025年10月21日—2025年10月26日,如有疑问请及时反馈)二、公示时间:2025年9月28日—2025年10月12日(含),若有疑问请在该时间段反馈,逾期视为放弃奖励!三、积分数据统计周期:仅统计8月25日—9月25日期间的考试数据(首次考取以上认证可参与积分),如积分数对不上请先检查是否考了重复的微认证或微认证证书未生成,微认证要证书生成才核算积分。另外如之前已经用云原生微认证兑换了云原生入门级开发者认证证书,本次再次考取云原生入门级开发者认证不积分。四、奖品发放:所有奖励将于活动公示期后陆续安排发放。五、积分兑换好礼(点此填写积分兑换问卷-仅可兑换1次,兑换后不可更改!填写时间截止到10月8日24:00,请及时填写) 【活动时间】2025年8月25日—2025年9月25日【活动福利】福利1:邀请报名有礼,最高可领1500元云资源代金券;福利2:考证积分兑好礼,最高可兑换2000元云资源券,或兑换华为手环9、华为耳机、双肩包等好礼福利3:完成云实验实操抽奖,抽华为耳机、定制水杯、定制双肩包、云宝盲盒等; 福利4:完成开发者空间案例实操抽奖,抽定制水杯、定制双肩包、定制雨伞、云宝盲盒等礼品。【点击链接立即报名】cid:link_1【微认证1元购】序号部分认证名称(含购买入口)考试通过可获得积分数价格1ModelArts实现零售商客户分群2282实现图片压缩及水印添加2283基于鲲鹏搭建zabbix分布式监控系统2284听歌识曲-抖音小视频背景音乐识别2285华为云数据库服务实践2286华为云计算服务实践2287点击了解更多认证(考取同样可以积分)—免费激活微认证不算积分2/点击前往活动页,可领取考试代金券及查看详细福利。部分奖品限量,先到先得,赶紧来参加吧!
-
案例介绍本案例使用开发者空间云开发环境提供的免费GaussDB数据库和HCE2.0开发环境进行本地部署python的Django,markdown,Pillow生态组件,部署一个Dog_Blog样式的博客系统。 案例内容一、概述1.1 案例介绍Django是一个高级的 Python Web 开源应用框架,可以快速开发安全和可维护的网站。Django负责处理网站开发中麻烦的部分,可以专注于编写应用程序,而无需重新开发。Pillow 是 Python 中较为基础的图像处理库,主要用于图像的基本处理,比如裁剪图像、调整图像大小和图像颜色处理等。markdown模块允许你将Markdown文本转换为HTML。Markdown是一种轻量级标记语言,它允许你使用易读易写的纯文本格式编写文档,然后转换成有效的HTML。本案例借助开发者空间云开发环境python中部署Django,Pillow,markdown,并与开发者空间提供的免费GaussDB数据库实例对接,完成一个博客系统的构建。华为开发者空间,是为全球开发者打造的专属开发者空间,致力于为每位开发者提供一台云开发环境、一套开发工具和云上存储空间,汇聚昇腾、鸿蒙、鲲鹏、GaussDB、欧拉等华为各项根技术的开发工具资源,并提供配套案例指导开发者 从开发编码到应用调测,基于华为根技术生态高效便捷的知识学习、技术体验、应用创新。1.2 适用对象企业个人开发者高校学生1.3 案例时间本案例总时长预计40分钟。1.4 案例流程说明:申请华为云开发者空间的GaussDB数据库;进行华为云开发者空间的云开发环境进行部署Django,Pillow,markdown部署,准备python华宁;启用博客系统,适配GaussDB。1.5 资源总览本案例预计花费0.8元。资源名称规格单价(元)时长(分钟)华为开发者空间 - 云开发环境鲲鹏通用计算增强型 kc1 | 2vCPUs | 4G | HCE2.0免费40华为开发者空间 - 生态版GaussDB单副本集中式版 | 4 vCPUs | 16G | HCE OS 64bit (200GB)免费40弹性公网IP按流量计费 5Mbit/s0.8元/GB40二、配置云开发环境 配置云开发环境根据案例《开发者空间 - 云开发环境使用指导》,请查看下面链接,配置云开发环境,并通过xshell等链接工具登录云开发环境。开发者空间 - 云开发环境使用指导三、 领取GaussDB数据库并配置3.1 免费领取GaussDB在线试用版(2025年 06月 21日 - 2025年 12月 31日)。华为开发者空间-GaussDB云数据库领取与使用指导注:部署的Django项目需要对接GaussDB,因此GaussDB需要绑定EIP,参考上述指导中领取部分第(5)步领取完成后在实例的基础信息页修改安全组,开放22端口与8000端口3.2 修改GaussDB数据库密码验证方式进入GaussDB数据库控制台,进入GaussDB实例,点击参数管理,修改GaussDB的密码验证参数 password_encryption_type 为 1。并点击保存生效。在弹出窗口输入yes点击确定3.3 初始化数据库及用户登陆数据库可参考3.1,登陆后创建数据库点击数据库名称进入库管理,然后点击sql窗口 创建blog用户create user blog with sysadmin password 'Gauss#123'; 3.4 验证云开发环境与GaussDB互通通过xshell或windows命令终端登录云开发环境,测试GaussDB能否连接,如下是通的四、 Python环境准备参考本地VSCode基于华为开发者空间云开发环境完成小程序开发,配置VScode开发环境4.1 创建项目虚拟环境因为业务场景的Python开发,多数都是构建一个大型应用程序,并且不希望各种组件的各种版本之间相互冲突,所以需要设置一个虚拟环境。先需要更新下载源。执行如下命令sudo yum -y updatesudo yum -y upgradepip3 install virtualenv -i cid:link_8 #安装virtualenvpython3 -m venv myenv #创建虚拟环境source myenv/bin/activate #激活环境环境激活后,用户名前会有(myenv)字样,如上图所示。4.2 python模块安装安装之前,需要更新下pip。python3 -m pip install --upgrade pip -i cid:link_8用python3自带的安装工具pip3,安装模块。pip3 install django==3.2 -i cid:link_8pip3 install markdown==3.6 -i cid:link_8pip3 install Pillow==10.3.0 -i cid:link_8注:安装django3.2版本,因为默认安装是django4.2.23,对PG内核版本要求至少在pg12以上,但GaussDB的pg内核是9.204,不满足django4.2.23需求。 4.3 安装GaussDB数据库对应的Python驱动GaussDB数据库对应的Python驱动为psycopg2。即Django组件允许通过psycopg2驱动,连接GaussDB数据库,并操作数据对象。不建议直接用pip去安装psycopg2驱动。所以从华为云GaussDB数据库官方文档中下载驱动。wget https://dbs-download.obs.cn-north-1.myhuaweicloud.com/GaussDB/1750076538851/GaussDB_driver.zipunzip GaussDB_driver.zipcd GaussDB_driver/Centralized/Hce2.0_arm_64/tar zxvf GaussDB-Kernel_505.2.1_Hce_64bit_Python.tar.gz解压后有两个文件夹:psycopg2:psycopg2库文件。lib:lib库文件。使用root用户将psycopg2复制到python安装目录下的site-packages文件夹下。sudo cp psycopg2 $(python3 -c 'import site; print(site.getsitepackages()[0])') -r修改psycopg2目录权限为755。sudo chmod 755 $(python3 -c 'import site; print(site.getsitepackages()[0])')/psycopg2 -R将psycopg2目录添加到环境变量$PYTHONPATH,并使之生效。export PYTHONPATH=$(python3 -c 'import site; print(site.getsitepackages()[0])'):$PYTHONPATH将解压后的上述lib目录,配置在环境变量LD_LIBRARY_PATH中。本案例对应的lib目录是/home/xxx/GaussDB_driver/Centralized/Hce2.0_arm_64/lib,读者根据自己实际情况做修改export LD_LIBRARY_PATH=/home/xxx/GaussDB_driver/Centralized/Hce2.0_arm_64/lib:$LD_LIBRARY_PATH在创建数据库连接之前,需要先加载如下数据库驱动程序:import psycopg2注意:如果引入psycopg2报找不到libpq.so.5.5,是因为环境变量LD_LIBRARY_PATH没有包含libpq.so.5.5的目录路径,由于上述步骤5用export设置的临时环境变量,所以在新的会话中需要重新设置LD_LIBRARY_PATH指定之前解压的驱动lib目录五、 程序使用项目开源地址:https://github.com/dawnstaryrx/django-dog-blog一个基于bear blog样式的Django博客系统5.1 下载代码点击downloadzip下载,并解压5.2 VScode上传代码 选择文件夹的程序包,拖入VSCcode5.3 VScode修代码打开dogBlog目录,双击setting.py文件,修改数据库信息,搜索DATABASES,用如下代码替换,如图 DATABASES = { "default": { 'ATOMIC_REQUESTS': 'True', 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'blog', # 数据库名 'USER': 'blog', # 用户名 'PASSWORD': 'xxx', # 密码 'HOST': 'xxx', # 生态版GaussDB弹性公网ip 'PORT': '8000', # GaussDB数据库端口 }然后ctrl+s或着点击关闭保存(一定要记着保存)5.4 初始化数据库表初始化数据库表cd django-dog-blog-main/python3 manage.py migrate 5.5 初始化admin登陆用户初始化admin登陆用户 INSERT INTO public.auth_user (password,last_login,is_superuser,username,first_name,last_name,email,is_staff,is_active,date_joined) VALUES ('pbkdf2_sha256$260000$Z3UtLaQ8NRzYybNQdbVZxG$N/RLkprzi/XKTije/0b6naQXKQyFvCD8dLboA08H4Uc=','2025-08-20 17:24:51.629',true,'admin','admin','admin','123@qq.com',true,true,'2025-08-20 16:43:33.724'); 5.6 启动博客系统进入代码目录启动代码python3 manage.py runserver 0.0.0.0:8080 参考开发者空间 - 云开发环境使用指导-第4章2小节做端口映射,创建隧道连接hdspace devenv start-tunnel --instance-id=bff5fbf4ee54491cbc3a70441cbea18a --remote-port=8080 --local-port=80浏览器输入http://localhost:80进行登陆输入用户密码,admin 密码Gauss#123进行登陆管理界面博客展示 我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 cid:link_7
-
直播主题:华为云开发者课程全解析与MCP协议揭秘直播讲师:阿兴 丨 华为云学堂技术讲师直播时间:2025.08.26 / 16:00-17:30直播链接:cid:link_0直播简介:基于云开发环境与全栈工具链深度体验昇腾鲲鹏等根技术生态, 三大课程体系:AI系列(DeepSeek/MCP)、鲲鹏调优、MySQL实战,涵盖人工智能系列含MCP智能体协议开发实战鲲鹏性能调优及MySQL数据库实战课程,重点介绍开发者空间三大系列课程与MCP协议揭秘并提供百万级DeepSeek Tokens资源,即刻预约直播开启高效开发之旅。直播亮点:1.开发者空间系列免费课程分享2.MCP协议揭秘3.华为根技术开发资源及工具等免费学习资源分享加入微信交流群:直播期间扫码入群,解锁更多隐藏福利哦~
-
前言- 云开发环境 是华为开发者空间提供远程云开发环境,开发者可在本地通过工具和浏览器多种形式接入开发环境,完成编码开发、远程操作、项目部署及本地访问等多个场景的作业活动,开发者可以轻松完成本地PC开发环境到云开发环境的无缝切换。那么今天大家就跟随我一起简单的走一遍基于华为开发者空间-云开发环境实现PyCharm SSH远程开发环境搭建!~一.开发者空间-云开发环境搭件华为开发者空间 是华为云为全球开发者打造的专属云上成长空间,空间深度整合了昇腾AI、鸿蒙、鲲鹏等华为根技术。2025HDC大会上,华为开发者空间迎来全面升级,新增AI原生应用引擎、AI Notebook、云开发环境、FunctionGraph云函数、Astro低代码等核心能力,并在算力、模型、平台、应用层实现全方位优化,助力开发者高效完成从编码到调测的全流程,打造智能AI应用开发新体验。华为开发者空间 - 云开发环境 提供“云IDE解决方案”,为开发者构建一个以开发者体验为核心、开放集成的云端开发平台生态。支持主流IDE、提供多样化入口、开放API/SDK,并与开发者深度合作持续优化,致力于让云端开发环境成为广大开发者的首选、高校愉悦的工作方式。下面我们就一起先来创建云开发环境吧!~1. 创建云开发环境华为开发者空间-云开发环境平台为开发者提供专属云端开发主机,集成主流IDE工具,支持多端访问和API/SDK调用,打造开箱即用的开发环境。首先我们登录华为开发者空间 - 云开发环境平台,在页面左侧菜单栏选择开发平台 - 计算,点击开发环境,进入开发环境界面。2.配置参数:2.1 在开发环境列表下方点击新建开发环境按钮,页面右侧弹出新建开发环境配置界面。2.2 在新建开发环境配置界面依次配置环境名称为PyCharmSSH,开放端口为8081(本案例中使用默认的22端口,此处可在端口范围8080-8089中任意选填),设置默认账号为developer,最后根据自己情况设置密码。2.3 创建完成后,在开发环境列表中将新增我们刚才创建的开发环境。3.云开发环境开机:在开发环境列表,PyCharmSSH开发环境的操作栏点击更多,选择开机。开发环境自动进入开机中状态(速度快的时间几秒就完成了,耐心等待下,此状态无需任何操作),完成开机后状态栏从已就绪自动变更为运行中。二.PC端创建和管理云开发环境1. 安装cli工具包hdspace.exe是一款为开发者设计的cli工具包,开发者可以通过hdspace.exe完成开发者空间云开发环境在PC端的创建和管理,并且开发者通过该工具可以建立与云开发环境的隧道,进而实现对云开发环境的远程操作(上传下载文件、编码编译执行等)。2. 下载hdspace.exe在Web端创建的云开发环境上点击【远程连接】-【立即下载】,将cli工具包下载到本地。(这里我以Windows x64为例)3. 配置环境变量:以Windows11为例,在搜索栏搜索环境变量,或者在控制面板中找到环境变量,点击系统变量中的path,新增环境变量。把hdspace.exe文件所在目录,添加到新增的环境变量中并保存确认。如下图所示:4. 确认cli工具包安装成功:进入终端执行命令hdspace,查看安装结果。hdspace至此,已经显示hdspace安装完成。5. 查看cli命令行列表:执行help命令可查看cli命令行列表。hdspace --help 除此之外,还有如下命令,在此不做赘述,在下面会逐一讲解。含义命令云开发环境顶层帮助hdspace devenv --help开发者空间配置帮助hdspace config --help开发者空间版本帮助hdspace version --help云开发环境列表帮助hdspace devenv list --help创建云开发环境帮助hdspace devenv create --help启动云开发环境帮助hdspace devenv start –help关闭云开发环境帮助hdspace devenv close --help删除云开发环境帮助hdspace devenv delete --help建立隧道帮助hdspace devenv start-tunnel --help6. 配置本地环境:在使用cli工具包创建和管理云开发环境之前,需要先在本地配置用户的AK/SK,获取用户远程操作权限,保证操作的安全性。6.1 获取AK/SK:AK是Access Key(访问密钥)的缩写, 用于标识用户身份的唯一ID, 通常公开传输;SK是Secret Key(秘密密钥)的缩写,用于生成请求签名的保密密钥,仅用户和服务端持有。其核心功能是通过对称加密机制验证请求发送者的合法性,防止未授权访问。开发者配置AK/SK,等同于在cli中进行身份信息认证,从而可以在cli端对云开发环境进行操作。可以参考获取AK/SK文档。打开下载文件,AK/SK如下展示:6.2 配置AK/SK,输入命令:hdspace config按照提示输入AK/SK,注意:SK输入后并不会回显,并且需要再次输入确认。如上图显示,则配置AK/SK成功。7. 管理云开发环境:7.1 使用以命令,我们可以查看可用的云开发环境信息:hdspace devenv list7.2 建立隧道连接:创建本地PC和远程云开发环境的隧道链接,建立隧道连接命令如下:hdspace devenv start-tunnel --instance-id=instanceId [--remote-port=远程端口] --local-port=自定义监听本地端口–instance-id(必选): 字符串,即为上述步骤中创建的云开发环境ID。–remote-port(可选): 字符串,可以选择端口(8080-8089), 如果不设置, 默认22。–local-port(可选): 字符串,本地监听端口(0-65535), 如果不设置, 默认22。成功建立隧道连接:7.3 测试SSH远程连接:通过如下指令测试连接。ssh {默认账号}@127.0.0.1 -p {localPort} 注:此处默认账号为步骤“1. 创建云开发环境”中设置的默认账号;localPort为上一步创建隧道时local-port对应设置的端口号(我这里是22)。实际开发中请注意替换。根据日志提示,输入yes和密码,此处密码为步骤“1.1 创建云开发环境”中设置的密码。三.PyCharm配置SSH安装PyCharm的流程这里我就不赘述了,咱们直接进入主题吧:1.New Project(新建项目):在PC本地PyCharm的顶部菜单栏File > New Project,弹出New Project配置对话框。(如果你汉化了就更简单)2.在New Project配置对话框中左侧新建菜单栏选择Pura Python,在右侧依次配置Location,Interpreter type选择Project venv,并在Python version的下拉三角中选择自己的Python版本(本案例中选用Python 3.12.10)。最后点击Create,完成案例创建。如果你不知道自己的python版本,也可以在终端通过命令查询:python --version3.等待PyCharm自动加载配置文件,加载完成后的状态如下:4.配置Configuration:在PyCharm的顶部菜单栏打开Tools > Deployment,选择Configuration…,弹出Deployment配置界面。5.在Deployment配置界面中,点击左上角的“+”,选择SFTP。6.在弹出的Create New Server对话框中输入Sever名称cloudDevEnv,开发者可根据自己的需求设置。点击OK按钮。7.在Deployment配置对话框中,左侧选中cloudDevEnv,在右侧选择Connection页签,点击参数SSH configuration右侧的“…”图标,打开SSH Configurations配置界面。8.在SSH Configurations配置界面,点击左上角的“+”,在其右侧设置配置参数:Host, 设置为默认值127.0.0.1;Port, 设置为22(此处port为步骤“测试SSH远程连接”中设置的端口);Username, 设置为developer(与步骤“创建云开发环境”中设置的默认账号保持一致);Authentication type, 选择Password;Password, 在步骤“创建云开发环境”中设置的密码。注意:如果这里“”测试连接“”不成功,可以再次使用命令连接下,再而进行测试:hdspace devenv start-tunnel --instance-id=instanceId [--remote-port=远程端口] --local-port=自定义监听本地端口9.在Deployment配置界面中,左侧选中cloudDevEnv,在右侧选择Mappings页签,设置其参数:Local path:本地python project对应的工程路径。Deployment path:云开发环境对应对的python project路径。点击右下角的OK按钮,完成Connection和Mappings配置。10.在PyCharm的顶部菜单栏打开Tools > Deployment,此时我们发现菜单中多了Upload、Download、Sync以及Browse Remote Host等几个新功能:Upload to cloudDevEnv:将本地代码上传到服务器。Download from cloudDevEnv:将远程服务器的代码拉取到本地。Sync with Deployed to cloudDevEnv:同步代码。11.我们在PyCharmSS工程的main.py中,增加一段代码。在PyCharm的顶部菜单栏打开Tools > Deployment,选择Upload to cloudDevEnv,控制台输出代码上传成功日志。示例代码:def main(): print("我是郑小健,这是我的测试文案") if __name__ == " __name__ ": main() 12.在PyCharm的顶部菜单栏打开Tools > Deployment,选择Browse Remote Host,在右侧Remot Host窗口找到对应的工程路径。打开main.py查看,代码上传成功。同理,在云开发环境端修改、编辑代码,可以使用Download from cloudDevEnv将代码拉取到本地。也可以通过Sync with Deployed to cloudDevEnv对比、同步代码。13.控制台测试连接:在PyCharm的顶部菜单栏打开Tools > Start SSH Session,在弹出的Select Host to Connect对话框中选择devenv@127.0.0.1:22。14.控制台输出上图SSH日志登录信息,控制台连接成功。15.接下来我们更新下python环境,为下一步配置远程解释器做准备。sudo yum update -ysudo yum install python3-devel python3-pip -y16.配置远程解释器:在PyCharm的顶部菜单栏选择File > Settings,打开设置界面。Settings设置界面,打开Project {项目名称} > Python Interpreter。在右侧的解释器配置界面,点击右侧的Add Interpreter,选择On SSH。17.在New Target:SSH界面,SSH connection选择Existing,SSH Server在其右侧的下拉三角下选择我们之前创建的devenv@127.0.0.1:22,点击Next。18.等待系统自动完成对SSH服务器进行自检,点击Next。19.在项目目录和Python运行时配置界面,左侧选中Virtualenv Enviroment。在其右侧:Enviroment:选择New,新建环境;Location:开发者可以通过右侧文件夹图标,可根据自己的需求在云开发环境中选择Python运行时环境目录;Base interpreter:一般系统会自动识别,可以根据实际选择云开发环境中已安装的版本;Sync folders:项目目录,可根据自己的需求在云开发环境中选择。20.点击【创建】,等待系统自动完成虚拟环境创建。21.等待远程解释器自动完成配置加载,点击右上角的运行按钮,控制台输出如下内容。到此我们就完成了整个流程的功能实现咯。~四.资源释放:在Windows PowerShell窗口通过hdspace devenv close命令释放云开发环境资源。hdspace devenv close --instance-id={instanceId} 当然我们也可以去后台检查或者操作关机哟,非常的简单吧:通过我这份实践流程,相信大家基于华为开发者空间-云开发环境实现PyCharm SSH远程开发环境搭建的认识会更加的轻松和掌握。我正在参加【案例共创】第6期 开发者空间-基于云开发环境和GaussDB构建应用 https://bbs.huaweicloud.com/forum/thread-0229189398343651003-1-1.html
-
体验华为开发者空间《【案例共创】基于华为云开发者空间-Versatile Agent开发平台构建昇腾C算子开发知识》案例,反馈改进建议,请直接在评论区反馈即可。体验指导:https://devstation.connect.huaweicloud.com/space/devportal/casecenter/452ae119cd3b4c009a3c8dc3fe3b7f64/1
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签