-
华为开发者空间是做什么的?开发者空间是基于构建华为统一开发者生态战略目标下推出的统一开发者入口平台,作为2D开发者触点,推广华为的根生态能力,免费提供相关便捷工具和资源,助力开发者快速体验使用华为根技术。空间提供各种华为根生态工具、预置各种根生态场景化资源,配套提供各种技术快速学习体验试用的手把手指导体验案例文档。提示我的使用时长即将耗尽了怎么办?当您的使用剩余时长小于1小时,点击延时按钮可增加15小时的使用时长,每个账号的云主机使用时长为180小时/年,如果超过这个上限您可以提交工单申请延时。如何给我的云主机重置系统?步骤:云主机关机(已就绪)状态下,点击云主机卡片上的重置按钮,然后重新配置云主机。我的工作空间页面主机配置信息与主机内不一致?X86版本云主机目前是2核4G;ARM版本云主机目前是4核8G。云主机可以使用SSH远程链接吗?当前暂不支持ssh远程链接功能,敬请期待。是否可以清除数据重新申领云主机?华为云能力,当前每个账号只可申领一台免费的云主机进行体验。暂不支持,您可以通过重置功能清除数据,重新配置云主机。或者升级专业会员,同时拥有基础主机和专业主机。在云主机搭建一个web应用,外网怎么访问云主机中的网站?当前暂不支持外网部访问,署服务器集群目前安全组只对云主机放开,云主机暂时未提供公网访问地址。开发桌面本身当前定位是个开发态机器,建议引导往ECS、fg serverless上做部署。开发者云主机中的应用都是免费的吗?系统默认应用都是免费体验的。登录云主机后忘记了账户密码怎么办?在领取完成后系统会使用默认账号进行登录,无需设置账号密码。如果您忘记了华为账号密码可以通过账号申诉找回您的账号或者进行密码重置。开发者空间云主机可以当做我的服务器吗?云主机可以为您提供开箱即用的便捷使用体验,无法保证承担服务器相关功能,如您需要使用服务器建议购买专用服务器资源进行使用。我在哪里查看云主机体检时长是多少?可以在云主机桌面右侧的工具栏中进行查看。当前是体验阶段,为鼓励使用,进入云主机后初次体验使用时长是15小时,当剩余时长小于1小时,点击延时按钮可增加15小时的时长,每个账号的云主机使用时长为180小时/年,如果超过这个上限您可以提交工单申请延时。是否可以进行协同办公?进入云主机后,可在上方工具栏的共享桌面选项中,通过远程桌面共享、远程桌面控制,进行协同办公。是否可以同时使用基础云主机和专业云主机?可以,您可以分别在浏览器中打开基础云主机和专业云主机,两台云主机的运行互不影响。云主机可以放大缩小或者适应全屏功能吗?当前暂不支持放大缩小和适应全屏功能,敬请期待。是否可以训练大模型?当前用作免费体验的基础版云主机服务不包含GPU、NPU资源,无法支持大多数大模型的训练部署,后续会上线包含更多配置规格的云主机可以支持大模型。云主机被我搞坏了怎么办?步骤:云主机关机(已就绪)状态下,点击云主机卡片上的重置按钮,然后重新配置云主机。云主机无法连接了怎么办?请依次尝试清空浏览器缓存、更换网络、重启云主机、自主重置等步骤,若均无法连接上云主机,您可以请在开发者空间中提交工单,我们将对您的云主机产生的问题进行解决。(重置云主机将导致您的数据丢失,请慎重申请)。初次申领云主机初始化了很久还没有完成怎么办?云主机初始化需要3~5分钟,请尝试继续等待安装完成后再体验云主机。开发者云主机使用过程中提示连接断开了怎么办?您可以点击重新连接,也可以更换网络后再进行尝试,我们也会持续对云主机的连接进行优化。如何上传文件至开发者云主机?参考本地与云主机间的文件交互指南,使用云存储空间进行本地与云主机之间文件交互。云主机长时间未使用是否会回收?如果领取后长时间未使用,会对云主机资源进行回收,后续如使用,需要您重新配置云主机。内存不够用,升级是否会导致数据丢失?可以选择升级专业会员,原云主机里的内容不会丢失,但长期不开机就会被回收,专业会员的云主机在会员有效期内不会被回收。隐藏文件夹里的文件不能上传云空间,是不是问题?云存储空间没有校验文件是否是隐藏状态,只要是符合上传格式的,都可以上传。是问题。开发空间中,已就绪时看不到主机的关机按钮。已就绪状态就是已关机状态,不展示关机按钮。关闭云主机浏览器页面后会自动关机吗?关闭浏览器中的云主机页面15分钟后会自动关机。提示当前账号异常,已被限制使用怎么办?您可以请在开发者空间中提交工单,选择账号问题 ,我们将对您的云主机产生的问题进行解决。本地内容怎么复制到云主机中?通过云主机提供的剪切板。云存储空间5G的云存储空间如何使用?您可以在云存储空间右侧点击打开按钮,然后进入空间使用,上传下载文件都可以,云主机通过浏览器也可打开使用。什么时候可以使用Windows系统主机?目前正在规划中,敬请期待。请问云主机上能连云数据库么?目前暂不支持,敬请期待。云主机连续输入中文无法完整输入内容?当前云主机无法通过外部输入法完整输入长文本内容,建议使用内部输入法。云主机如何使用输入法?如何切换输入法的中英文?请确认本地的输入法为英文状态,才可调起云主机输入法;请按“shift键”进行云主机输入法中/英文切换。arm不支持复制粘贴吗?ubuntu的arm不支持复制粘贴。github网络访问延时高,可以优化吗?github这个域名解析都是海外的,访问的时候很多链路也都有问题,跨境链路网络上没有办法优化。临时规避:可以从服务器里ping下它解析的这些ip,看看哪个ip不丢包,时延小一点,就用hosts绑定上强行解析到这个ip上使用,具体ip如下:20.205.243.166140.82.121.320.27.177.1134.237.22.3820.201.28.151140.82.112.4140.82.113.420.200.245.247140.82.116.4140.82.114.320.26.156.21520.87.245.0199.59.148.9199.59.149.235点击链接,立即开通开发者空间
-
使用场景分类部分FAQ示意链接云主机云主机配置类是否能变更操作系统?如何获取ROOT权限?如何设置云主机的ROOT密码?……点击查看云主机使用提示我的使用时长即将耗尽了怎么办?可以使用SSH远程链接吗?外网怎么访问云主机中的网站?……点击查看云主机工具类有哪些开发工具可以选择?是否可以安装其他软件?云主机配置选错了可以修改吗?……点击查看开发环境操作指导如何创建开发环境?如何获取AKSK如何与三方工具连接?……点击查看开发平台常见问题开通AI Agent服务时,系统总是提示开通失败?大模型Agent免费额度有限制吗?云主机是和云开发环境,时间是共享的吗?……点击查看
-
华为云空间云主机配置,目前是只有Ubuntu系统吗,是否能变更Ubuntu系统?目前支持Ubuntu和Euler系统,可以在配置云主机时进行选择。领取云主机配置完成后不知道账号和密码?在领取完成后系统会使用默认账号进行登录,无需设置账号密码。升级云主机系统后,使用sudo xxx指令需要输入密码,升级之前不需要?升级中更改了我们的默认配置,用户需要重新配置免密。建议用户升级时,保留默认配置,则升级后免密仍可生效。如何获取ROOT权限?当前不提供ROOT密码,可使用sudo命令进行提权操作。是否可以设置关机密码?云主机关机无需设置关机密码。使用sudo do-release-upgrade将Ubuntu22.04升级到24.01之后,在使用sudo xxx指令需要输入密码,升级之前不需要升级中更改了云主机的默认配置,需要您重新配置免密。建议您在升级时,保留默认配置,则升级后免密仍可生效。我的云主机是否可以升级内核?当前云主机不支持升级内核,强行升级可能会导致系统崩溃,无法登陆。您可以开通专业会员,获得专业主机。我是否可以设置云主机的ROOT密码?当前是体验阶段,暂不支持修改ROOT密码,自行修改可能会导致云主机桌面无法正常连接。云主机默认developer账号的密码能否修改?当前是体验阶段,自行修改可能会导致云主机桌面无法正常连接。我的云主机存储空间大小是多少?当前云主机存储容量为40G。我的云主机配置可以升级吗?为了更好的服务体验,开发者空间现已推出“开发者专业会员”服务,可将云主机配置CPU提升到8vCPUs,内存提升到16GB。实名认证对年龄有要求吗?个人实名认证有年龄限制,禁止18岁以下的未成年人进行实名认证。提示“开通失败,该手机号不能使用,请更换其他手机号开通华为云”手机号不能注册?华为云禁止使用虚拟号段注册,使用传统号段注册即可。开通华为云时提示“开通失败,系统检测到您当前的IP开通华为云数量超限,请稍晚再开通华为云”?同一个公网ip下不能大量注册账号。建议不要连接WiFi,改为使用手机流量进行注册。云主机的工作台是否可以增加性能指标监控功能?功能正在建设中,未来会引入到开发者空间供大家体验使用,敬请期待。 如需要进行查看,可以 进入主机,打开终端,输入top命令查看。云主机的带宽是多少?可以进入云主机后在上方悬浮工具栏中网路图标,点击详情查看实时带宽情况。如果是新用户,注册完之后就可以直接跳转到云主机领取页面,不需要再进行扫码吗?新用户注册完之后,会跳转到实名认证界面,华为大云能力,点击返回原页面即可跳转到领取云主机页面,不需要进行扫码。如果是老用户,忘记密码并重置登录后需要重新扫码领取还是直接可以领取?重置密码后登录的是华为大云,需要重新扫码。沃土计划提示“账号异常,已被限制使用”账号涉及黑产,需在华为云提工单处理。注册领取云主机的账号时,之前从未使用过华为手机,也没有注册过该账号,但是显示已注册过华为云账户不是独立的,注册过华为账户或者其他生态的账户,正常可以在各个生态页面登录。您可以在华为云提工单,查一下注册时间,看看是不是二次放号。登录华为账号时,提示“需要家长或监护人同意并接受新的服务条款,才能。。。请输入家长或监护人的华为账号(xxx)密码”该华为账号是儿童账号,可以通过给华为云提单,确定账号的具体问题。按高校专属推广链接点进云主机领取页面之后,需要进行学生认证。但他本来身份是老师,这样会不会影响其账号内已有的权益?不会。CSDN登录时,华为云服务授权时,提示“该手机号/邮箱已被华为报备,无法创建客户”该手机号,已经被华为线上电网销跟进,不允许直接关联伙伴。可以换个手机号先注册关联,不做实名,关联后再尝试实名。让客户需要关联的这个伙伴账号在伙伴中心进行报备(由伙伴去报备),报备通过后就可以关联了。点击链接,立即开通开发者空间
-
本期获奖情况如下,恭喜各位获奖者~~~~温馨提示:获奖用户请提前实名认证,以便顺利发放代金券!代金券一周之内发放至论坛私信(右上角我的社区-我的消息-私信中领取),礼品已在申请流程中,请耐心等待,谢谢!多篇文章的本期以最高奖项的发放,不叠加。如发布3篇,2篇参与奖,1篇三等奖,按三等奖发放。类型主题作者帖子标题奖项文章风吹雨【案例共创】基于MaaS结合开发者空间Astro低代码平台完成学生成绩评价系统二等奖xiaowuyun【案例共创】基于MaaS结合开发者空间Astro低代码平台开发高考大学择校推荐系统二等奖鸢尾离夏【案例共创】MaaS结合Astro便捷发布诗词生成网站三等奖小草飞上天【案例共创】基于MaaS结合Astro平台快速创建景点推荐智能化系统三等奖夏同学【案例共创】基于华为云开发者空间 Astro 低代码以及 MaaS 大模型完成精确针对个人健身的建议咨询三等奖banjin【案例共创】基于MaaS结合开发者空间Astro低代码平台完成sql生成助手三等奖春夏秋【案例共创】 通过华为云Astro轻应用开发答题程序参与奖视频风吹雨【【案例共创】基于MaaS结合开发者空间Astro低代码平台完成学生成绩评价系统】 https://www.bilibili.com/video/BV19XxezNEqc/?share_source=copy_web&vd_source=9ef6e22a14b1d402db7a4e9b9be6365d三等奖xiaowuyun【华为开发者空间:基于MaaS结合开发者空间Astro低代码平台开发高考大学择校推荐系统-哔哩哔哩】 https://b23.tv/slFunqX三等奖鸢尾离夏【基于华为云Maas平台和Astro低代码快速创建诗词生成智能体网站】 https://www.bilibili.com/video/BV1Aux9zTEtA/?share_source=copy_web&vd_source=09222d9a5fee477ee7d89f546f75f986参与奖小草飞上天【华为云 MaaS 结合 Astro 快速打造官方网站-哔哩哔哩】 https://b23.tv/dZ9sdc5参与奖 了解案例共创活动为了让更多开发者能够更轻松、更高效地理解和使用我们的文档,进而提升云产品的整体使用体验,我们致力于进一步优化和完善官方产品文档。在此过程中,我们诚挚地邀请广大开发者积极参与,通过亲身体验云产品,编写实践案例或体验评测。一旦您的案例经过专家评审团的认可与采纳,将有以下三点:优质案例将被正式收录至官方案例库,供广大开发者学习。优质案例将选送到在华为云站内外10+个技术社区推荐,给予百万级流量资源。以上案例我们都将注明原作者名字,实现与开发者共创官方文档。 参与者不仅有机会获得每月活动礼品,还有可能被评为年度内容贡献官,享受更多荣誉和奖励,获得更多合作机会。我们期待着与您一起,共同打造更加优质、高效的云服务体验。参与投稿方式第1步:(已注册并实名可跳过)华为云账号实名认证,点击这里。(已设置可跳过)登录后设置社区昵称,点我设置。第2步:点击报名填写报名问卷,提供礼品发放地址等信息第3步:开启您的云端体验,分享实践案例,点我写帖子。版块选择“社区活动”分类选择“案例共创”帖子标题在前面添加【案例共创】(一定要加,方便识别参与活动的帖子)文末可添加活动名称及链接地址,如“我正在参加【案例共创】第7期 基于MaaS商用服务 + 华为开发者空间 - Astro 低代码开发平台构建低代码应用 cid:link_7”【如您在体验中有任何产品问题,欢迎在论坛发布问题求助帖(帖子分类选择问题求助)咨询产品专家,如发现任何体验不友好、产品Bug、文档页面错漏等情况,欢迎通过云声平台反馈给我们,还有机会领取云声专属礼品!活动相关咨询可以论坛私信“论坛小助手SUN”】Ø华为云产品介绍:华为开发者空间 - Astro 低代码开发:华为开发者空间 - Astro 低代码开发平台通过提供的界面、逻辑、对象等可视化编排工具,以“拖、拉、拽”的方式来快速构建应用,从而实现所见即所得的应用开发构建体验。Astro 低代码平台开发操作参考材料:界面:cid:link_8逻辑 - 编排:cid:link_9数据 - 对象:cid:link_10数据 - 结构体:cid:link_11集成 - 连接器:cid:link_12华为云MaaS商用Token服务:ModelArts Studio大模型即服务平台(后续简称为MaaS服务),提供端到端的大模型生产工具链和昇腾算力资源,并预置了当前主流的第三方开源大模型,支持大模型数据生产、微调、提示词工程、应用编排等功能。用户可以基于MaaS平台开箱即用,对预置大模型进行二次开发,用于生产商用。商用百万Token代金券免费领取链接(可每周领取):DeepSeek-R1/V3-64K百万tokens代金券:cid:link_4代金券使用说明:购买DeepSeek Tokens套餐包,DeepSeek-R1 轻量体验包(¥7.00)或DeepSeek-V3 轻量体验包(¥3.50);进入ModelArts Studio服务页面—在线推理—商用服务—DeepSeek-V3/R1-64K—开通服务—API Key配置;Ø 应用构建要求结合自己的工作实践,使用领取的Maas免费百万Token,结合华为开发者空间Astro低代码平台完成应用构建,应用构建类型和主题参考如下:1、领取MaaS免费商用Token服务 + 开发者空间 Astro 低代码开发构建页面和业务逻辑 + 完成应用开发;主题场景包括但不限于以下方向(仅做参考):场景1:大模型服务+实际业务,比如职业咨询;Ø 手册案例模版参考:MaaS结合Astro低代码平台的应用实践:1)案例名称:基于maas结合开发者空间Astro低代码平台完成求职咨询地址:cid:link_2简介:本案例开发一个大学生就业问卷调查应用,针对往届生和应届生设置问题,并根据问卷调查的结果,基于人工智能大模型DeepSeek智能输出就业择业建议注意:案例手册请大家一定要按照案例模版的格式进行编写注意:案例手册请大家一定要按照案例模版的格式进行编写注意:案例手册请大家一定要按照案例模版的格式进行编写 Ø活动流程(全年征集,每月一期评选)投稿时间:2025年9月1日-10月08日联合评审:2025年10月09日-10月15日奖项公示:2025年10月16-20日奖品发放:获奖名单公布后15个工作日内寄出Ø 投稿规则手册类投稿:1)构建的应用需要完成打包,并以附件的形式附在文章末尾。打包参考:cid:link_202)手册包含案例场景和方案以及用到的华为云产品或者开源框架简述。3)手册包含开发过程实际操作描述(文字描述+截图+代码)。4)手册的案例实操需要完整体现,可根据手册上手并体验。视频类投稿:1)构建的应用需要完成发布,视频包含应用在线情况的展示。2)视频包好案例场景和方案以及用到的华为云产品或者开源框架简述。3)视频包含开发过程实际操作。4)视频的案例实操需要完整体现,可根据视频上手并体验。 Ø 评奖规则(规则已更新,添加案例影响性条目)标准类型手册形式分值释义视频形式分值释义分值阶梯总分值案例影响性根据浏览数和评论数评价案例影响性1:浏览数达到100;2:浏览数达到200;根据浏览数和评论数评价案例影响性1:浏览数达到150;2:浏览数达到300;1-2/0.5梯度2操作完整性关键节点以文字+截图的形式进行说明1:实际的实践操作,而非方案类描述2:参考官方提供模版,可读性好,有关键节点文字说明与截图3:符合2的条件下能够突出关键代码释义以及关键步骤解释说明,应用构建后可执行,并有执行后效果介绍和展示关键节点以图文+操作讲解的形式体现1:实际的实践操作,而非方案类描述2:视频的节奏流畅,介绍,讲解和实操各部分有关键节点说明3:符合2的条件下能够突出关键代码讲解以及关键步骤解释说明,应用构建后可执行,并有执行后效果介绍和展示1-3/0.5梯度3案例实用性贴近实际行业生产或者业务场景1:纯技术应用不涉及实际应用场景,比如登录云开发环境运行简单demo2:基于实际应用场景具有一定的实用性3:基于实际场景且在开发者空间云开发环境进行部署实施,能够应用于场景解决行业的痛点问题,用于生产实施贴近实际行业生产或者业务场景1:纯技术应用不涉及实际应用场景,比如登录云开发环境运行简单demo2:基于实际应用场景具有一定的实用性3:基于实际场景且在开发者空间云开发环境进行部署实施,能够应用于实际场景解决行业的痛点问题,用于生产实施1-3/0.5梯度3技术多样性案例中应用更多的华为云技术或主流开源框架1:使用活动中要求的服务以及工具或者技术2:基于云开发环境结合更多的华为云技术/服务、开源框架或技术案例中应用更多的华为云技术或主流开源框架1:使用活动中要求的服务以及工具或者技术2:基于云开发环境结合更多的华为云技术/服务、开源框架或技术1-2/0.5梯度2 奖项设置 视频案例奖项:文章案例奖项: 注:每期评审专家根据每篇文章评选文章分数及等级,每期文章质量不一,不排除有某个奖项轮空的情况,如第X期文章质量在A级和B级,即S和S+级轮空。同一个内容以分别以文章和视频的方式参加活动,奖项会已视频为先,文章奖项会降一个等级。(如:同时提交视频和文章两个内容,视频获得二等奖,文章即获得三等奖,以此类推。)代金券及周边礼物发放对象为:已完成实名认证的华为云用户。发放到填写报名问卷的账号及收货地址中,礼包类礼品均为实物礼品。
-
在万物互联时代,用户需求正从“人找服务”逐步向“服务找人”转变。HarmonyOS 以用户为中心,依托POI、信标、鸿蒙标签、NFC iTAP等技术打造近场服务能力,将近场服务融入用户日常生活场景,悄然改变众多领域的服务体验。本期近场服务聚焦商超、文旅、餐饮三大行业的典型应用场景,带你感受HarmonyOS近场服务带来的体验提升。一、智慧商超:为商铺装上“智能导购” 在传统商超综合体中,商铺客流大多依赖品牌影响力和区位优势,普通商铺难以有效吸引顾客驻足。 而当商铺部署信标设备后,用户进入信标连接范围即可收到传输信号,通过“小艺建议”获取门店活动、特色服务等推荐,助力商家在用户消费决策前实现精准曝光,显著提升店铺引流能力,为会员转化和成交率带来新增长点。 二、智慧文旅:打造沉浸式游览体验 假期出游高峰时,排队购票导致入园拥堵、景区导览设置不清导致错过打卡点等都会影响游客的游览体验。 近场服务基于POI位置推荐可在游客靠近景区附近时通过小艺建议获取购票服务卡片推荐,一键直达购票页面,比传统线上购票软件减少约50%操作步骤。进入景区游览时,游客也可以基于景区内不同景点的POI点位推荐一键跳转至景区元服务详情页,当前景点讲解、后续景点推荐、游览路线推荐等一目了然,告别盲目寻找和人工问询。 三、智慧餐饮:一碰直达,极速点餐 餐饮门店可在餐桌或入口处设置HarmonyOS标签,用户通过手机“碰一碰”即可快速直达商家元服务页面。 消费者无需排队点单,手机“碰一碰”即可实现会员一键入会、获取优惠套餐、快速点餐等。不仅大大缩短用户操作步骤,提升了用户体验,也帮助商家大幅提升会员转化与订单效率,实现用户与商家的双赢。 HarmonyOS近场服务在以上行业应用场景中展示了强大的适配性和创新价值。除上述典型案例场景之外,还广泛应用在智慧办公、运动健康、本地生活、政务民生等领域。欢迎开发者点击下方链接了解并接入使用,与HarmonyOS一起共建共享鸿蒙新世界! 👉 点击了解更多并申请接入:申请开通权限-近场服务 - 华为HarmonyOS开发者 (huawei.com) AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
对H5页面占比高的APP而言,“加载慢”是用户体验的“头号杀手”——转圈的加载动画、迟迟不显示的内容,很容易让用户直接退出。为解决这一痛点,AppGallery Connect推出高性能Web容器组件FastWeb,专为H5页面提速而生,帮开发者搞定H5优化,让用户告别“加载卡顿”烦恼,体验更丝滑。一、先搞懂:什么是FastWeb组件?FastWeb是基于OpenHarmony开发的“高性能Web容器”,适用于对H5页面有性能优化需求(加载提速)的场景。像电商APP的商品详情页、资讯新闻列表页、工具类功能操作页等,只要是以H5形式呈现且对页面性能优化有诉求,希望提升加载速度,FastWeb都能派上用场。它聚焦网络大资源的“提速”核心,而非复杂业务逻辑的处理,旨在帮助大家用轻量化开发实现加载优化。二、两种使用方式:按需选择,灵活配置考虑到不同APP的H5开发现状,FastWeb提供两种灵活方案,无论全面改造还是增量式“迭代开发”,都带来了不错的提升效果。实验数据显示,某APP首次打开且无缓存时,直接加载Web页面需5413.58ms,多次打开有缓存时仍需1345.93ms,这是因为该方式要在页面加载时才拉起渲染进程、发起资源请求,额外增加了加载耗时;而使用FastWeb组件后,首次打开(无缓存)加载页面加载时间缩短49.9%;多次打开(有缓存)页面加载时间缩短39.7%。具体数据如下: 方式一:全面改造,解锁全能力若想彻底发挥FastWeb的优化实力,即便H5已封装过Web容器,也能通过此方式“全方位提速”。它会调用预启动、预渲染、预编译JavaScript生成字节码缓存、离线资源拦截注入四大能力,从“提前准备”到“资源复用”拉满效率。操作很简单:APP启动时(或合适时机)创建空的ArkWeb组件“预热”,展示H5页面时直接挂载即可。需注意删除原有Web容器,将属性和事件写入FastWeb暴露对象,适合有调整空间的团队。方式二:增量式“迭代开发”,快速提效如果已经将H5页面封装成Web容器,并希望在不修改原页面的基础上进行优化,你可以通过FastWeb的预编译JavaScript生成字节码缓存、离线资源拦截注入两大能力,实现提速。操作逻辑同上:提前创建空ArkWeb组件,可以在App启动时创建,或者其他合适的页面创建。展示H5时直接用原有页面,无需额外调整。适合追求“低成本快速优化”的团队,兼顾效果与业务稳定性。三、实用建议:避坑指南,用得更顺手想让FastWeb稳定发挥提速效果,这几个细节要注意:FastWeb组件的核心优势在于网络大资源的预加载能力,而非复杂业务逻辑处理,建议优先用于首页H5、高频核心页等“优化关键路径”,能让提速效果更突出。若应用涉及桥接功能需求,优先选方式二,避免改动原有容器,确保通信稳定的同时,不影响加载速度提升。创建FastWeb组件将占用内存(每个FastWeb组件大约200MB)和计算资源,建议避免一次性创建大量FastWeb组件,按页面访问频率合理规划,避免出现“为了快而牺牲流畅”的情况。对H5多的APP来说,FastWeb不是“可选优化项”,而是“刚需组件”。它无需复杂适配,两种方式覆盖不同开发场景。若你正为H5加载慢头疼,不妨试试FastWeb——让用户告别等待,让APP体验再上台阶。AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
在用户注意力稀缺的今天,如何让每一次触达都精准转化为应用内的活跃行为?华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务,提供“应用链接”和“元服务链接”,可直接跳转HarmonyOS应用或者跳转元服务,有效简化用户访问路径。无论是内容分享、游戏互动还是服务直达,App Linking都能提供有力支持。它不仅能帮助开发者提升应用的竞争力,还能为用户带来更便捷、高效的使用体验。 今天就来盘点下,App Linking 到底有哪些好用的全场景链接技巧!一、社交互动篇:2个技巧解锁社交分享新玩法 社交分享是用户传播的核心场景,但传统分享常因 “操作复杂、跳转卡顿” 流失用户。App Linking 通过2个技巧,让社交分享既有趣又高效,轻松提升裂变转化效果。App Linking+华为分享,助力线上社交裂变 核心功能:依托 HarmonyOS 系统级分享面板,支持直接生成带应用 / 元服务入口的分享链接,可无缝分享至微信、畅联等主流社交 AppApp Linking+碰一碰分享,社交分享新体验 核心功能:两部设备轻轻一碰即可传递链接,实现 “一碰即传、极简操作”,带来全新的社交互动体验,趣味性与便捷性兼顾。 点击查看场景案例: 华为视频碰一碰,让跨设备视频分享一步到位 二、服务触达篇:3 个方案助力服务直达 App Linking 通过3种针对性方案,实现无需提前打开 App,没有复杂跳转过程,就可直达服务。App Linking+系统扫码,一扫直达目标页面 核心功能:多渠道扫码,负一屏、控制中心、系统相机均可通过扫码,无需用户打开App,通过系统扫码直达应用的核心页面。App Linking+智能消息,一步直达服务页面 核心功能:智能消息作为营销活动的优秀载体。从消息一键直达服务,体验友好。可以提高营销转化率。App Linking+鸿蒙标签,服务一碰即达 核心功能:即碰即走,方便快捷;碰扫合一,多样化体验。便捷使用,需要碰一碰服务标签即可获取服务信息。 点击查看场景案例:美团一扫即达,服务快人一步,操作效率提升30%以上 三、进阶攻略篇:2 个工具让分享链路精准触达直达应用市场:目标应用 “点击即达”,减少流量流失 核心功能:当成功配置App Linking应用链接后,可以构建App Linking直达链接。当应用已安装时,点击链接直接跳转应用;当应用未安装时,点击链接跳转应用市场下载详情页,引导用户下载应用。延迟链接:跳转 “不跑偏”,提升转化效率 核心功能:当被分享用户未安装应用时,通过延迟链接能力,应用首次打开时,系统仍能获取用户之前点击的应用相关链接。在获取链接后,应用可直接跳转至对应的详情页,无需先跳转至应用首页,从而提升用户体验和链接的转化率。 点击查看场景案例: App Linking助力华为阅读分享链路精准触达,操作步骤减43%! 对于开发者而言,App Linking 不只是简单的链接工具,更是提升用户使用体验的核心利器。它打通 “用户触达” 与 “服务落地”,让应用与用户连接更高效。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章 ——App Linking 。 AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
一、概述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
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签