Unity2D初级背包设计中篇 MVC分层撰写(万字详解)

         本人能力有限,如有不足还请斧正,理论分析链接如下:

        Unity2D初级背包设计前篇 理论分析-CSDN博客

目录

1.M层:数据存储  

物品

仓库容器

加载方式

2.M层:逻辑撰写

InventoryManager 仓库的管理

SlotData 物品的增删查改*(逻辑层)

3.V层:搭建UI

        1.SlotUI 将物品绘制到格子上

        2.三个类

BarUI工具栏的选中与数字切换

    BarSoltUI格子高亮 

        BagUI 将格子数据绘制到界面上

4.C层:玩家控制类

ItemMoveHandler 物品的增删查改*(表现层)

         Plyaer 拾取与丢弃


1.M层:数据存储  

物品

        一个2D的物品必不可少的就是类型,图标,物体(这个指Gameobjcet和其名字)堆叠数量

        拓展思路:稀有度,价值,耐久,描述,重量,

using UnityEngine;
/// <summary>
/// 不同的物品可以有不同的类型 以用作其他逻辑的判断
/// 举例:食物可以吃但通常不能打人
///       武器通常不能吃但可以打人
/// </summary>
public enum ItemType{a,b,c,d
}
/// <summary>
///
/// </summary>
[CreateAssetMenu()]
public class ItemData:ScriptableObject
{public ItemType itmeType;public Sprite itemSprite;public GameObject itemPrefab;public int maxCount =1;//最大堆叠数量:默认:1
}

        另外可能需要一个外部类去定义其属性,比如可拾取的,不可拾取的 这个的作用体现在玩家拾取方面,需要提前知道一下

public class Pickable : MonoBehaviour
{public ItemType thisItemType;
}

         so对象举例

        

        

仓库容器

        先认识一个单词:Inventory,仓库,库存 在本文中特指背包类

        之所以要对Inventory写为So,是因为游戏中可能存在许多的仓库,在玩家手中叫做背包,物品栏,在NPC手中就可能叫做商店,锻造了,因此:

using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class InventoryData :ScriptableObject{//TODO :拓展 仓库名称,最大容量,自动扩容public List<SlotData> slotList;
}

        创建两个

        可以看到其容量是我们自定义的,也就是写死的没有自动扩容功能

        因此在今后的拓展之中可以优化

加载方式

2.M层:逻辑撰写

InventoryManager 仓库的管理

       我们需要一个仓库管理器去管理所有的背包所以用到了InventoryData 对象

        另外,管理所有物品的时候也应需要一个容器去预加载,所以用到了字典

  可以先不看这一行:我们需要为外部提供提供向背包添加物品的方法,所以用到了单例模式

using System.Collections.Generic;
using UnityEngine;public class InventoryManager : MonoBehaviour {private static InventoryManager instance;public static InventoryManager Instance => instance;public InventoryData BagInventory;public InventoryData ToolBarInventory;public Dictionary<ItemType, ItemData> itemDataDict = new Dictionary<ItemType, ItemData>();private void Awake() {if (instance == null) {instance = this;}else {Destroy(this);}InitInventoryData();}private void Start() {}/// <summary>/// 加载本地物品和已有的背包数据/// </summary>private void InitInventoryData() {ItemData[] itemDatas = Resources.LoadAll<ItemData>("Itmes");foreach (var singleItem in itemDatas) {itemDataDict.Add(singleItem.itmeType, singleItem);}BagInventory = Resources.Load<InventoryData>("Inventorys/MyInventory");ToolBarInventory= Resources.Load<InventoryData>("Inventorys/ToolBarInventory");}/// <summary>/// 通过指定的物品类型从字典中获取物品数据/// </summary>/// <param name="type">物品类型</param>/// <returns>如果找到则返回物品数据,否则返回null</returns>private ItemData GetItem(ItemType type) {if (itemDataDict.TryGetValue(type, out var item))return item;else {Debug.LogError("未找到指定物品");return null;}}/// <summary>/// 向背包中添加物品/// </summary>/// <param name="itemType">要添加的物品类型</param>public void AddItemToInventory(ItemType itemType) {ItemData aItem = GetItem(itemType);// 情况1:字典中有该物品且格子未满 如果格子满了将会走情况2foreach (var slot in BagInventory.slotList) {if (slot.item == aItem && slot.CanAddItem()) {slot.Add();return;}}// 情况2:格子为空,添加物品foreach (var slot in BagInventory.slotList) {if (slot.count == 0) {slot.AddItem(aItem);return;}}// 情况3:背包已满Debug.LogWarning($"未能成功添加物品 {aItem.name} 因为 {BagInventory.name} 已经满了");}
}

SlotData 物品的增删查改*(逻辑层)

        这个类将目光聚焦于背包中的格子,因为它才是背包的最小单位

        

        格子需要持有物品类,并且有一个当前数量的变量

        每次修改格子物品信息的时候需要给到其通知(也就是M层----->V层这一步),所以需要做发布者发布一个委托,让V层去监听  

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[Serializable]
public class SlotData {public ItemData item;public int currentCount = 0; // 物品数量private Action OnChange;#region 增(Add)// 添加物品到槽位public void Add(int numToAdd = 1) {this.currentCount += numToAdd;OnChange?.Invoke();}// 设置槽位的物品和数量public void AddItem(ItemData item, int count = 1) {this.item = item;this.currentCount = count;OnChange?.Invoke();}#endregion#region 删(Remove)// 减少槽位中的物品数量public void Reduce(int numToReduce = 1) {currentCount -= numToReduce;if (currentCount == 0) {Clear();}else {OnChange?.Invoke();}}// 清空槽位public void Clear() {item = null;currentCount = 0;OnChange?.Invoke();}#endregion#region 查(Check)// 检查槽位是否为空public bool IsEmpty() {return currentCount == 0;}// 检查槽位是否可以添加物品public bool CanAddItem() {return currentCount < item.maxCount;}// 获取槽位的空余空间public int GetFreeSpace() {return item.maxCount - currentCount;}#endregion#region 改(Update)// 移动槽位数据public void MoveSlot(SlotData data) {this.item = data.item;this.currentCount = data.currentCount;OnChange?.Invoke();}// 添加监听器public void AddListener(Action OnChange) {this.OnChange = OnChange;}#endregion
}

        就这么点东西,至此M层就算是写完辣

3.V层:搭建UI

         

        1.SlotUI 将物品绘制到格子上

         自然其应持有SlotData类,并订阅其发布的委托,回调函数就是ChangeUI方法

        SetData实现的就是C---->M层,GetData是V---->C层

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;public class SlotUI : MonoBehaviour,IPointerClickHandler
{protected SlotData slotData;protected Image icon;protected TextMeshProUGUI num;private void Start() {icon = transform.Find("icon").GetComponent<Image>();num = transform.Find("num").GetComponent<TextMeshProUGUI>();}public SlotData GetData(){ return slotData;}/// <summary>/// 为该脚本上的对象赋值一个SlotData/// </summary>public void SetData(SlotData slotData) { this.slotData = slotData;//事件监听 - 订阅者slotData.AddListener(UpdateUI2Slot);UpdateUI2Slot();}/ <summary>/ 监听对象/ </summary>//public void ChangeUI(){//    UpdateUI2Slot();//}private void UpdateUI2Slot(){if (slotData==null || slotData.item == null || slotData.currentCount <= 0) {icon.enabled = false;num.enabled = false;}else {icon.enabled = true;num.enabled = true;icon.sprite = slotData.item.itemSprite;num.text = slotData.currentCount.ToString();}}public void OnPointerClick(PointerEventData eventData) {Debug.Log("发生了点击");ItemMoveHandler.Instance.OnSlotClick(this);}
}

选中后将物品依附到鼠标上面 

        2.三个类

BarUI工具栏的选中与数字切换

using System.Collections.Generic;
using UnityEngine;public class BarUI : MonoBehaviour
{//工具栏ui列表 先给这个列表对应好ui格子,之后将数据列表粘贴到ui列表中 并更新UI即完成可视化[SerializeField]private List<BarSlotUI> barSlotUIList;[SerializeField] private GameObject ContentList;[SerializeField] private BarSlotUI curSelectBarSlotUI;//当前选中的工具栏的格子// Start is called once before the first execution of Update after the MonoBehaviour is createdvoid Start(){barSlotUIList = new List<BarSlotUI>();ContentList =transform.Find("Bar/ContentList").gameObject;InitSlotUI();}private void Update() {SelectBarSlot();}/// <summary>/// 默认curSelectBarSlotUI为空,所以首次不会进入第二个if/// 当第二次选中格子时,curSelectBarSlotUI指向第一个格子,所以不为空就高亮第一个格子/// </summary>public void SelectBarSlot(){for (int i = (int)KeyCode.Alpha1; i < (int)KeyCode.Alpha9 + 1; i++) {if (Input.GetKeyDown((KeyCode)i)) {if (curSelectBarSlotUI != null) {curSelectBarSlotUI.BarSlotLight();}int index = i - (int)KeyCode.Alpha1;curSelectBarSlotUI = barSlotUIList[index];curSelectBarSlotUI.BarSlotDark();}}}public void InitSlotUI() {if (ContentList != null) {foreach (BarSlotUI barSlotUI in ContentList.GetComponentsInChildren<SlotUI>()) {barSlotUIList.Add(barSlotUI);}}UpdataUI();}public void UpdataUI() {for (int i = 0; i < InventoryManager.Instance.ToolBarInventory.slotList.Count; i++) {barSlotUIList[i].SetData(InventoryManager.Instance.ToolBarInventory.slotList[i]);}}}

    BarSoltUI格子高亮 

using TMPro;
using UnityEngine;
using UnityEngine.UI;public class BarSlotUI : SlotUI
{[SerializeField] private Sprite slotLight;[SerializeField] private Sprite slotDark;[SerializeField]private Image thisImage;// Start is called once before the first execution of Update after the MonoBehaviour is createdvoid Start(){InitBar_Slot();}public void InitBar_Slot(){this.icon = transform.Find("Bar_icon").GetComponent<Image>();this.num = transform.Find("Bar_num").GetComponent<TextMeshProUGUI>();thisImage =GetComponent<Image>();slotLight = Resources.Load<Sprite>("SlotUI/slotLight");slotDark = Resources.Load<Sprite>("SlotUI/slotDark");}public void BarSlotLight(){thisImage.sprite = slotLight;}public void BarSlotDark() {thisImage.sprite = slotDark;}
}

        BagUI 将格子数据绘制到界面上

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class BagUI : MonoBehaviour {[SerializeField] private Button close;[SerializeField] private GameObject BG;[SerializeField] private GameObject slotGrid;[SerializeField] private List<SlotUI> soltuiList = new List<SlotUI>();// Start is called once before the first execution of Update after the MonoBehaviour is createdvoid Start() {InitElement();InitSlotUI();}// Update is called once per framevoid Update() {ColseBag();}public void InitElement() {BG = transform.Find("BG").gameObject;close = transform.Find("BG/BgElement/Close").GetComponent<Button>();slotGrid = transform.Find("BG/SlotGrid").gameObject;if (close != null) {close.onClick.AddListener(() => {if (BG != null)BG.SetActive(!BG.activeSelf);else {Debug.LogWarning("没找到BG对象");return;}});}elseDebug.LogWarning("没有加载到close按钮");}public void UpdataUI() {for (int i = 0; i < InventoryManager.Instance.BagInventory.slotList.Count; i++) {soltuiList[i].SetData(InventoryManager.Instance.BagInventory.slotList[i]);}}public void InitSlotUI() {if (slotGrid != null) {foreach (SlotUI slotUi in slotGrid.GetComponentsInChildren<SlotUI>()) {soltuiList.Add(slotUi);}}UpdataUI();}public void ColseBag() {if (Input.GetKeyDown(KeyCode.Tab))BG.SetActive(!BG.activeSelf);}
}

4.C层:玩家控制类

ItemMoveHandler 物品的增删查改*(表现层)

        这个类其实应该好好讲一讲,但是笔者写了一个多小时雀氏有点累了,反正都是一些常见的逻辑 所以用ai给到注释,如果前面的代码都理解了,那么这里一点也不难

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;public class ItemMoveHandler : MonoBehaviour {// 单例模式的实例public static ItemMoveHandler Instance {get; private set;}// 图标private Image icon;// 选中的槽数据private SlotData selectedSlotData;// 玩家对象private Player player;// 控制键是否按下private bool isCtrlDown = false;// Awake方法在脚本实例化时调用private void Awake() {Instance = this;icon = GetComponentInChildren<Image>();HideIcon();player = GameObject.FindAnyObjectByType<Player>();}// Update方法在每帧调用private void Update() {// 如果图标启用,更新图标位置if (icon.enabled) {Vector2 position;RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), Input.mousePosition,null,out position);icon.GetComponent<RectTransform>().anchoredPosition = position;}// 左键点击时,如果没有点击UI元素,丢弃物品if (Input.GetMouseButtonDown(0)) {if (EventSystem.current.IsPointerOverGameObject() == false) {ThrowItem();}}// 检测Ctrl键按下和松开if (Input.GetKeyDown(KeyCode.LeftControl)) {isCtrlDown = true;}if (Input.GetKeyUp(KeyCode.LeftControl)) {isCtrlDown = false;}// 右键点击时,强制清空手上的物品if (Input.GetMouseButtonDown(1)) {ClearHandForced();}}// 槽点击事件处理public void OnSlotClick(SlotUI slotui) {// 判断手上是否有物品if (selectedSlotData != null) {// 手上有物品if (slotui.GetData().IsEmpty()) {// 当前点击了一个空格子MoveToEmptySlot(selectedSlotData, slotui.GetData());}else {// 当前点击了一个非空格子if (selectedSlotData == slotui.GetData())return;else {// 点击了别的格子 且 两个格子的物品相同if (selectedSlotData.item == slotui.GetData().item) {MoveToNotEmptySlot(selectedSlotData, slotui.GetData());}else {SwitchData(selectedSlotData, slotui.GetData());}}}}else {// 手上没有物品if (slotui.GetData().IsEmpty())return;selectedSlotData = slotui.GetData();ShowIcon(selectedSlotData.item.itemSprite);}}// 隐藏图标void HideIcon() {icon.enabled = false;}// 显示图标void ShowIcon(Sprite sprite) {icon.sprite = sprite;icon.enabled = true;}// 清空手上的物品void ClearHand() {if (selectedSlotData.IsEmpty()) {HideIcon();selectedSlotData = null;}}// 强制清空手上的物品void ClearHandForced() {HideIcon();selectedSlotData = null;}// 丢弃物品private void ThrowItem() {if (selectedSlotData != null) {GameObject prefab = selectedSlotData.item.itemPrefab;int count = selectedSlotData.currentCount;if (isCtrlDown) {player.ThrowItem2Creat(prefab, 1);selectedSlotData.Reduce();}else {player.ThrowItem2Creat(prefab, count);selectedSlotData.Clear();}ClearHand();}}// 移动物品到空槽private void MoveToEmptySlot(SlotData fromData, SlotData toData) {if (isCtrlDown) {toData.AddItem(fromData.item);fromData.Reduce();}else {toData.MoveSlot(fromData);fromData.Clear();}ClearHand();}// 移动物品到非空槽private void MoveToNotEmptySlot(SlotData fromData, SlotData toData) {if (isCtrlDown) {if (toData.CanAddItem()) {toData.Add();fromData.Reduce();}}else {int freespace = toData.GetFreeSpace();if (fromData.currentCount > freespace) {toData.Add(freespace);fromData.Reduce(freespace);}else {toData.Add(fromData.currentCount);fromData.Clear();}}ClearHand();}// 交换槽数据private void SwitchData(SlotData data1, SlotData data2) {ItemData item = data1.item;int count = data1.currentCount;data1.MoveSlot(data2);data2.AddItem(item, count);ClearHandForced();}
}

         Plyaer 拾取与丢弃

using UnityEngine;public class Player : MonoBehaviour
{private void OnTriggerEnter2D(Collider2D collision) {Debug.Log("进入了触发检测");if (collision.CompareTag("Item")){Debug.Log("检测到了物品");InventoryManager.Instance.AddItemToInventory(collision.GetComponent<Pickable>().thisItemType);Destroy(collision.gameObject);}}/// <summary>/// 丢弃背包的物品 并在场景中实例化出来/// </summary>/// <param name="itemPrefab">丢弃对象</param>/// <param name="count">丢弃数量</param>public void ThrowItem2Creat(GameObject itemPrefab,int count){ for(int i = 0;  i < count; i++){GameObject go  =Instantiate(itemPrefab);Vector2 dir = Random.insideUnitCircle.normalized * 0.8f;go.transform.position = new Vector3(dir.x,dir.y,0);} }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/891897.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入理解 Linux 管道:创建与应用详解(匿名管道进程池)

在现代操作系统中&#xff0c;进程间通信&#xff08;IPC&#xff09;是实现多任务、多进程协作的关键技术之一。Linux 提供了多种 IPC 机制&#xff0c;本博客将帮助您详细的理解进程间通信的原理 首先&#xff0c;在学习管道之前&#xff0c;我们先理解一下管道的存在是为了什…

SWM221系列芯片之电机应用及控制

经过对SWM221系列的强大性能及外设资源&#xff0c;TFTLCD彩屏显示及控制进行了整体介绍后&#xff0c;新迎来我们的电控篇---SWM221系列芯片之电机应用及控制。在微控制器市场面临性能、集成度与成本挑战的当下&#xff0c;SWM221系列芯片以其卓越性能与创新设计&#xff0c;受…

Trimble天宝X9三维扫描仪为建筑外墙检测提供了全新的解决方案【沪敖3D】

随着城市化进程的快速推进&#xff0c;城市高层建筑不断增多&#xff0c;对建筑质量的要求也在不断提高。建筑外墙检测&#xff0c;如平整度和垂直度检测&#xff0c;是衡量建筑质量的重要指标之一。传统人工检测方法不仅操作繁琐、效率低下&#xff0c;还难以全面反映墙体的真…

机器人手眼标定

机器人手眼标定 一、机器人手眼标定1. 眼在手上标定基本原理2. 眼在手外标定基本原理 二、眼在手外标定实验三、标定精度分析 一、机器人手眼标定 要实现由图像目标点到实际物体上抓取点之间的坐标转换&#xff0c;就必须拥有准确的相机内外参信息。其中内参是相机内部的基本参…

unity中的UI系统---GUI

一、工作原理和主要作用 1.GUI是什么&#xff1f; 即即时模式游戏用户交互界面&#xff08;IMGUI&#xff09;&#xff0c;在unity中一般简称为GUI&#xff0c;它是一个代码驱动的UI系统。 2.GUI的主要作用 2.1作为程序员的调试工具&#xff0c;创建游戏内调测试工具 2.2为…

Java开发 PDF文件生成方案

业务需求背景 业务端需要能够将考试答卷内容按指定格式呈现并导出为pdf格式进行存档&#xff0c;作为紧急需求插入。导出内容存在样式复杂性&#xff0c;包括特定的字体&#xff08;中文&#xff09;、字号、颜色&#xff0c;页面得有页眉、页码&#xff0c;数据需要进行表格聚…

SpringCloud微服务架构

文章目录 认识微服务&#xff1a;SpringCloud 服务拆分及远程调用实现夸远程服务调用使用RestTemplateEureka注册中心 搭建EruekaServer注册服务服务发现 Ribbon负载均衡 修改负载均衡规则解饿加载 Nacos注册中心&#xff08;nacos一部分功能&#xff09; 服务注册到nacosnacos…

【设计模式-02】23 种设计模式的分类和功能

在软件工程领域&#xff0c;设计模式是解决常见设计问题的经典方案。1994 年&#xff0c;Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides&#xff08;四人帮&#xff0c;GoF&#xff09;在《设计模式&#xff1a;可复用面向对象软件的基础》一书中系统性地总结了…

简历_专业技能_熟悉分布式锁Redisson的原理以及使用

系列博客目录 文章目录 系列博客目录怎么样才能够在简历上写熟悉redisson的应用以及原理1. 清晰描述技能与经验示例&#xff1a; 2. 列举具体应用场景示例项目经验&#xff1a; 3. 展示你对原理的理解示例&#xff1a; 4. 用简历中的关键词突出你的能力示例段落&#xff1a; 5.…

分布式任务调度xxl-job入门案例

XXL-JOB是一个分布式任务调度平台&#xff0c;简单来说就是可以在你指定的时间内调用某个功能&#xff0c;就例如购物某个商品的限时抢购从什么时候开始以及结束抢购类似于这样的。 下面是它的一个仓库地址 http://gitee.com/xuxueli0323/xxl-job 下载之后将项目导入进idea中&…

瑞芯微rk3566刷机流程(黑豹X2)

文章目录 概要 刷机方式 卡刷流程 线刷流程 小结 概要 记录rk3566刷机的过程&#xff0c;纯纯的小白&#xff0c;艰难而有意义的一天。 刷机方式 1、卡刷&#xff08;tf卡&#xff09; 2、线刷&#xff08;双公头usb线&#xff09; 卡刷流程 1、下载armbian镜像 1、…

计算机网络——数据链路层-流量控制和可靠传输

一、流量控制 流量控制是指由接收方及时控制发送方发送数据的速率&#xff0c;使接收方来得及接受。 • 停止等待流量控制 • 滑动窗口流量控制 1、停止—等待流量控制 停止-等待流量控制的基本原理是发送方每发出一帧后&#xff0c;就要等待接收方的应答信号&#xff…

GPT系统重大升级,开创国内先河:o1支持图片识别功能正式上线

文章目录 零、前言一、授权码登录体验优化&#xff1a;一步直达聊天界面二、全新“项目”功能&#xff1a;让工作更有条理三、语音功能升级&#xff1a;全新交互体验四、o1支持图片识别五、总结 零、前言 我是虚竹哥&#xff0c;目标是带十万人玩转ChatGPT。 亲爱的用户&…

RabbitMQ-基本使用

RabbitMQ: One broker to queue them all | RabbitMQ 官方 安装到Docker中 docker run \-e RABBITMQ_DEFAULT_USERrabbit \-e RABBITMQ_DEFAULT_PASSrabbit \-v mq-plugins:/plugins \--name mq \--hostname mq \-p 15672:15672 \-p 5672:5672 \--network mynet\-d \rabbitmq:3…

弹性云服务器ECS“规格”

规格详细资料&#xff1a;规格清单&#xff08;x86&#xff09;_弹性云服务器 ECS_华为云 通用计算型 各规格详细介绍请参见通用计算型。 规格名称 计算 磁盘类型 网络 通用计算型X1 CPU/内存配比&#xff1a;自定义vCPU数量范围&#xff1a;1-16处理器&#xff1a;第三…

Java SpringBoot使用Apache POI导入导出Excel文件

点击下载《Java SpringBoot使用Apache POI导入导出Excel文件(源代码)》 1. Apache POI 简介 Apache POI 是一个强大的 Java 库&#xff0c;用于处理 Microsoft Office 文档&#xff0c;包括 Excel 文件&#xff08;.xls 和 .xlsx&#xff09;。在 Java Spring Boot 项目中&am…

在 macOS 中,设置自动将文件夹排在最前

文章目录 1、第一步访达设置2、第二步排序方式 需要两步设置 1、第一步访达设置 按名称排序的窗口中 2、第二步排序方式 选择名称

汇编环境搭建

学习视频 将MASM所在目录 指定为C盘

Sentinel-5P遥感数据下载及预处理教程【20250105】

Sentinel-5P是欧空局&#xff08;Europe Space Agency&#xff0c;ESA&#xff09;于2017年10月13日发射的一颗全球大气污染监测卫星。卫星搭载了对流层观测仪&#xff08;Tropospheric Monitoring Instrument&#xff0c;TROPOMI&#xff09;&#xff0c;可以有效的观测全球各…

java项目之高校心理教育辅导系统的设计与实现(springboot+mybatis+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的闲一品交易平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 高校心理教育辅导系统的设…