diff --git a/Build_God_Api/Build_God_Api/Controllers/BagController.cs b/Build_God_Api/Build_God_Api/Controllers/BagController.cs index e21f492..a1eb287 100644 --- a/Build_God_Api/Build_God_Api/Controllers/BagController.cs +++ b/Build_God_Api/Build_God_Api/Controllers/BagController.cs @@ -100,6 +100,13 @@ namespace Build_God_Api.Controllers return await service.RemoveItemFromBag(characterBagId, itemId); } + [HttpPost("{characterBagId}/items/destroy")] + [Authorize] + public async Task> DestroyBagItem(int characterBagId, [FromBody] DestroyBagItemDto dto) + { + return await service.DestroyBagItem(characterBagId, dto.ItemType, dto.ItemDbId); + } + // ============ 枚举 ============ [HttpGet("rarities")] @@ -129,4 +136,10 @@ namespace Build_God_Api.Controllers public int ItemId { get; set; } public int Quantity { get; set; } } + + public class DestroyBagItemDto + { + public int ItemType { get; set; } + public int ItemDbId { get; set; } + } } diff --git a/Build_God_Api/Build_God_Api/Controllers/ShopController.cs b/Build_God_Api/Build_God_Api/Controllers/ShopController.cs new file mode 100644 index 0000000..6311d8f --- /dev/null +++ b/Build_God_Api/Build_God_Api/Controllers/ShopController.cs @@ -0,0 +1,78 @@ +using Build_God_Api.DB; +using Build_God_Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Build_God_Api.Controllers +{ + [ApiController] + [Route("api/god/[controller]")] + public class ShopController( + IShopService service, + ICurrentUserService currentUserService + ) : ControllerBase + { + private readonly IShopService service = service; + private readonly ICurrentUserService currentUserService = currentUserService; + + // ============ 商店物品配置管理(后台) ============ + + [HttpGet("admin/all")] + [Authorize(Roles = "admin")] + public async Task>> GetAllShopItems() + { + return await service.GetAllShopItems(); + } + + [HttpGet("admin/{id}")] + [Authorize(Roles = "admin")] + public async Task> GetShopItemById(int id) + { + return await service.GetShopItemById(id); + } + + [HttpPost("admin")] + [Authorize(Roles = "admin")] + public async Task> CreateShopItem([FromBody] ShopItem item) + { + return await service.CreateShopItem(item); + } + + [HttpPut("admin/{id}")] + [Authorize(Roles = "admin")] + public async Task> UpdateShopItem(int id, [FromBody] ShopItem item) + { + item.Id = id; + return await service.UpdateShopItem(item); + } + + [HttpDelete("admin/{id}")] + [Authorize(Roles = "admin")] + public async Task> DeleteShopItem(int id) + { + return await service.DeleteShopItem(id); + } + + // ============ 玩家商店 ============ + + [HttpGet] + [Authorize] + public async Task> GetCharacterShop() + { + var characterList = await service.GetCharacterShop(currentUserService.UserId); + return characterList; + } + + [HttpPost("buy")] + [Authorize] + public async Task> BuyItem([FromBody] ShopBuyDto dto) + { + return await service.BuyItem(currentUserService.UserId, dto.ShopItemId); + } + } + + public class ShopBuyDto + { + public int ShopItemId { get; set; } + } +} \ No newline at end of file diff --git a/Build_God_Api/Build_God_Api/DB/BagItem.cs b/Build_God_Api/Build_God_Api/DB/BagItem.cs index f7fadfb..8dbabbb 100644 --- a/Build_God_Api/Build_God_Api/DB/BagItem.cs +++ b/Build_God_Api/Build_God_Api/DB/BagItem.cs @@ -27,6 +27,11 @@ namespace Build_God_Api.DB /// 数量 /// public int Quantity { get; set; } + + /// + /// 是否绑定(绑定后不可交易/拍卖) + /// + public bool IsBound { get; set; } } /// diff --git a/Build_God_Api/Build_God_Api/DB/Equipment.cs b/Build_God_Api/Build_God_Api/DB/Equipment.cs index 78e5ab7..4b737b3 100644 --- a/Build_God_Api/Build_God_Api/DB/Equipment.cs +++ b/Build_God_Api/Build_God_Api/DB/Equipment.cs @@ -121,6 +121,11 @@ namespace Build_God_Api.DB /// [SugarColumn(IsNullable = true)] public int? SetId { get; set; } + + /// + /// 是否绑定(绑定后不可交易/拍卖) + /// + public bool IsBound { get; set; } } /// diff --git a/Build_God_Api/Build_God_Api/DB/Shop.cs b/Build_God_Api/Build_God_Api/DB/Shop.cs new file mode 100644 index 0000000..5cea33f --- /dev/null +++ b/Build_God_Api/Build_God_Api/DB/Shop.cs @@ -0,0 +1,89 @@ +using SqlSugar; +using System.ComponentModel; + +namespace Build_God_Api.DB +{ + /// + /// 商店物品配置表 - 维护可售卖的商品库 + /// + public class ShopItem : BaseEntity + { + /// + /// 物品类型 + /// + public BagItemType ItemType { get; set; } + + /// + /// 物品ID (装备模板ID/丹药ID/垃圾ID) + /// + public int ItemId { get; set; } + + /// + /// 灵石价格 + /// + public int Price { get; set; } + + /// + /// 每日限购次数(0表示不限购) + /// + public int DailyLimit { get; set; } + + /// + /// 是否启用 + /// + public bool IsActive { get; set; } + + /// + /// 排序权重(越大越优先出现) + /// + public int SortOrder { get; set; } + } + + /// + /// 玩家商店状态 - 记录每个玩家的商店数据 + /// + public class CharacterShop : BaseEntity + { + /// + /// 角色ID + /// + public int CharacterId { get; set; } + + /// + /// 上次刷新时间 + /// + public DateTime LastRefreshTime { get; set; } + + /// + /// 当前商品列表(JSON)- 存储当前刷出来的商品及库存 + /// 格式: [{"ShopItemId":1,"Stock":5},{"ShopItemId":2,"Stock":3}] + /// + public string ItemsJson { get; set; } = "[]"; + } + + /// + /// 玩家商店购买记录 - 记录每日购买次数 + /// + public class CharacterShopPurchaseLog : BaseEntity + { + /// + /// 角色ID + /// + public int CharacterId { get; set; } + + /// + /// 商店物品配置ID + /// + public int ShopItemId { get; set; } + + /// + /// 当日购买次数 + /// + public int PurchasedCount { get; set; } + + /// + /// 记录日期(yyyy-MM-dd) + /// + public string PurchaseDate { get; set; } = string.Empty; + } +} diff --git a/Build_God_Api/Build_God_Api/Program.cs b/Build_God_Api/Build_God_Api/Program.cs index cfb8e94..52dd858 100644 --- a/Build_God_Api/Build_God_Api/Program.cs +++ b/Build_God_Api/Build_God_Api/Program.cs @@ -152,6 +152,7 @@ namespace Build_God_Api builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); diff --git a/Build_God_Api/Build_God_Api/Services/BagService.cs b/Build_God_Api/Build_God_Api/Services/BagService.cs index da42629..1699b1c 100644 --- a/Build_God_Api/Build_God_Api/Services/BagService.cs +++ b/Build_God_Api/Build_God_Api/Services/BagService.cs @@ -26,6 +26,9 @@ namespace Build_God_Api.Services // 物品数量管理 Task GetItemQuantity(int characterBagId, int itemType, int itemId); Task ReduceItemQuantity(int characterBagId, int itemType, int itemId, int quantity); + + // 物品销毁 + Task DestroyBagItem(int characterBagId, int itemType, int itemDbId); } public class BagItemDto @@ -349,5 +352,31 @@ namespace Build_God_Api.Services return true; } + + public async Task DestroyBagItem(int characterBagId, int itemType, int itemDbId) + { + if (itemType == (int)BagItemType.Equipment) + { + var equipment = await db.Queryable() + .FirstAsync(x => x.Id == itemDbId && x.CharacterBagId == characterBagId); + + if (equipment == null) + throw new Exception("装备不存在"); + + await db.Deleteable(equipment).ExecuteCommandAsync(); + } + else + { + var item = await db.Queryable() + .FirstAsync(x => x.Id == itemDbId && x.CharacterBagId == characterBagId); + + if (item == null) + throw new Exception("物品不存在"); + + await db.Deleteable(item).ExecuteCommandAsync(); + } + + return true; + } } } diff --git a/Build_God_Api/Build_God_Api/Services/ShopService.cs b/Build_God_Api/Build_God_Api/Services/ShopService.cs new file mode 100644 index 0000000..9a8a39f --- /dev/null +++ b/Build_God_Api/Build_God_Api/Services/ShopService.cs @@ -0,0 +1,353 @@ +using Build_God_Api.DB; +using Build_God_Api.Dto; +using SqlSugar; +using System.Text.Json; + +namespace Build_God_Api.Services +{ + public interface IShopService + { + // 商店物品配置管理(后台) + Task> GetAllShopItems(); + Task GetShopItemById(int id); + Task CreateShopItem(ShopItem item); + Task UpdateShopItem(ShopItem item); + Task DeleteShopItem(int id); + + // 玩家商店 + Task GetCharacterShop(int characterId); + Task BuyItem(int characterId, int shopItemId); + } + + public class ShopItemDisplayDto + { + public int ShopItemId { get; set; } + public int ItemType { get; set; } + public int ItemId { get; set; } + public string? ItemName { get; set; } + public int Price { get; set; } + public int DailyLimit { get; set; } + public int PurchasedToday { get; set; } + public int RemainingStock { get; set; } + public string? Icon { get; set; } + public int? ItemRarity { get; set; } + } + + public class ShopDto + { + public int CharacterId { get; set; } + public DateTime LastRefreshTime { get; set; } + public List Items { get; set; } = new(); + } + + public class ShopCurrentItem + { + public int ShopItemId { get; set; } + public int Stock { get; set; } + } + + public class ShopService(ISqlSugarClient db, IBagService bagService) : IShopService + { + private readonly ISqlSugarClient db = db; + private readonly IBagService bagService = bagService; + private const int RefreshIntervalHours = 4; + private const int ShopItemCount = 6; + + // ============ 商店物品配置管理 ============ + + public async Task> GetAllShopItems() + { + return await db.Queryable() + .OrderBy(x => x.SortOrder, OrderByType.Desc) + .ToListAsync(); + } + + public async Task GetShopItemById(int id) + { + return await db.Queryable().FirstAsync(x => x.Id == id); + } + + public async Task CreateShopItem(ShopItem item) + { + await db.Insertable(item).ExecuteCommandAsync(); + return true; + } + + public async Task UpdateShopItem(ShopItem item) + { + var existing = await GetShopItemById(item.Id); + if (existing == null) + throw new Exception("商店物品不存在"); + + existing.ItemType = item.ItemType; + existing.ItemId = item.ItemId; + existing.Price = item.Price; + existing.DailyLimit = item.DailyLimit; + existing.IsActive = item.IsActive; + existing.SortOrder = item.SortOrder; + + await db.Updateable(existing).ExecuteCommandAsync(); + return true; + } + + public async Task DeleteShopItem(int id) + { + await db.Deleteable().Where(x => x.Id == id).ExecuteCommandAsync(); + return true; + } + + // ============ 玩家商店 ============ + + public async Task GetCharacterShop(int characterId) + { + var characterShop = await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId); + + var now = DateTime.Now; + var needRefresh = characterShop == null || + (now - characterShop.LastRefreshTime).TotalHours >= RefreshIntervalHours; + + if (needRefresh) + { + characterShop = await RefreshShop(characterId); + } + + return await BuildShopDto(characterId, characterShop); + } + + public async Task BuyItem(int characterId, int shopItemId) + { + var character = await db.Queryable().FirstAsync(x => x.Id == characterId) + ?? throw new Exception("角色不存在"); + + var shopItem = await GetShopItemById(shopItemId) + ?? throw new Exception("商店物品不存在"); + + if (!shopItem.IsActive) + throw new Exception("该物品已下架"); + + // 检查灵石是否足够 + if (character.Money < shopItem.Price) + throw new Exception("灵石不足"); + + var characterShop = await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId) + ?? throw new Exception("玩家商店数据不存在"); + + // 检查库存 + var currentItems = JsonSerializer.Deserialize>(characterShop.ItemsJson) ?? new(); + var currentItem = currentItems.FirstOrDefault(x => x.ShopItemId == shopItemId); + if (currentItem == null || currentItem.Stock <= 0) + throw new Exception("该物品已售罄"); + + // 检查限购 + var purchaseLog = shopItem.DailyLimit > 0 ? await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId + && x.ShopItemId == shopItemId + && x.PurchaseDate == DateTime.Now.ToString("yyyy-MM-dd")) : null; + + if (shopItem.DailyLimit > 0) + { + var purchasedCount = purchaseLog?.PurchasedCount ?? 0; + if (purchasedCount >= shopItem.DailyLimit) + throw new Exception("已达到每日购买上限"); + } + + // 扣除灵石 + character.Money -= shopItem.Price; + await db.Updateable(character).ExecuteCommandAsync(); + + // 添加物品到背包 + var characterBag = await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId) + ?? throw new Exception("角色背包不存在"); + + // 装备特殊处理:创建装备实例 + if (shopItem.ItemType == BagItemType.Equipment) + { + await CreateEquipmentInstance(characterBag.Id, shopItem.ItemId); + } + else + { + await bagService.AddItemToBag(characterBag.Id, (int)shopItem.ItemType, shopItem.ItemId, 1); + } + + // 更新库存 + currentItem.Stock--; + characterShop.ItemsJson = JsonSerializer.Serialize(currentItems); + await db.Updateable(characterShop).ExecuteCommandAsync(); + + // 记录购买次数 + var today2 = DateTime.Now.ToString("yyyy-MM-dd"); + if (purchaseLog == null) + { + purchaseLog = new CharacterShopPurchaseLog + { + CharacterId = characterId, + ShopItemId = shopItemId, + PurchasedCount = 1, + PurchaseDate = today2 + }; + await db.Insertable(purchaseLog).ExecuteCommandAsync(); + } + else + { + purchaseLog.PurchasedCount++; + await db.Updateable(purchaseLog).ExecuteCommandAsync(); + } + + return true; + } + + // ============ 私有方法 ============ + + private async Task RefreshShop(int characterId) + { + var now = DateTime.Now; + + // 获取所有启用的商品,按权重排序后随机抽取 + var allItems = await db.Queryable() + .Where(x => x.IsActive) + .OrderBy(x => x.SortOrder, OrderByType.Desc) + .ToListAsync(); + + if (!allItems.Any()) + { + var newCharacterShop = new CharacterShop + { + CharacterId = characterId, + LastRefreshTime = now, + ItemsJson = "[]" + }; + await db.Insertable(newCharacterShop).ExecuteCommandAsync(); + return newCharacterShop; + } + + // 随机抽取商品 + var selectedItems = new List(); + var random = new Random(); + var count = Math.Min(ShopItemCount, allItems.Count); + var shuffled = allItems.OrderBy(_ => random.Next()).Take(count).ToList(); + + foreach (var item in shuffled) + { + selectedItems.Add(new ShopCurrentItem + { + ShopItemId = item.Id, + Stock = 99 // 库存充足 + }); + } + + var characterShop = await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId); + + if (characterShop == null) + { + characterShop = new CharacterShop + { + CharacterId = characterId, + LastRefreshTime = now, + ItemsJson = JsonSerializer.Serialize(selectedItems) + }; + await db.Insertable(characterShop).ExecuteCommandAsync(); + } + else + { + characterShop.LastRefreshTime = now; + characterShop.ItemsJson = JsonSerializer.Serialize(selectedItems); + await db.Updateable(characterShop).ExecuteCommandAsync(); + } + + return characterShop; + } + + private async Task BuildShopDto(int characterId, CharacterShop characterShop) + { + var currentItems = JsonSerializer.Deserialize>(characterShop.ItemsJson) ?? new(); + var today = DateTime.Now.ToString("yyyy-MM-dd"); + + var result = new ShopDto + { + CharacterId = characterId, + LastRefreshTime = characterShop.LastRefreshTime, + Items = new List() + }; + + foreach (var current in currentItems) + { + var shopItem = await GetShopItemById(current.ShopItemId); + if (shopItem == null || !shopItem.IsActive) + continue; + + var display = new ShopItemDisplayDto + { + ShopItemId = shopItem.Id, + ItemType = (int)shopItem.ItemType, + ItemId = shopItem.ItemId, + Price = shopItem.Price, + DailyLimit = shopItem.DailyLimit, + RemainingStock = current.Stock + }; + + // 获取物品详情 + if (shopItem.ItemType == BagItemType.Equipment) + { + var equip = await db.Queryable().FirstAsync(x => x.Id == shopItem.ItemId); + display.ItemName = equip?.Name; + display.Icon = equip?.Icon; + display.ItemRarity = equip != null ? (int)equip.Rarity : null; + } + else if (shopItem.ItemType == BagItemType.Pill) + { + var pill = await db.Queryable().FirstAsync(x => x.Id == shopItem.ItemId); + display.ItemName = pill?.Name; + display.Icon = pill?.Icon; + } + else if (shopItem.ItemType == BagItemType.Scrap) + { + var scrap = await db.Queryable().FirstAsync(x => x.Id == shopItem.ItemId); + display.ItemName = scrap?.Name; + display.Icon = scrap?.Icon; + } + + // 获取今日购买次数 + if (shopItem.DailyLimit > 0) + { + var purchaseLog = await db.Queryable() + .FirstAsync(x => x.CharacterId == characterId + && x.ShopItemId == shopItem.Id + && x.PurchaseDate == today); + display.PurchasedToday = purchaseLog?.PurchasedCount ?? 0; + } + + result.Items.Add(display); + } + + return result; + } + + private async Task CreateEquipmentInstance(int characterBagId, int equipmentTemplateId) + { + var template = await db.Queryable() + .FirstAsync(x => x.Id == equipmentTemplateId) + ?? throw new Exception("装备模板不存在"); + + var instance = new EquipmentInstance + { + CharacterBagId = characterBagId, + EquipmentTemplateId = equipmentTemplateId, + Name = template.Name, + Type = template.Type, + Rarity = template.Rarity, + Attributes = "[]", + EnhanceLevel = 0, + EnhanceBonusPercent = 0, + RequirdLevelId = template.RequirdLevelId, + SetId = template.SetId, + IsBound = true + }; + + await db.Insertable(instance).ExecuteCommandAsync(); + } + } +} \ No newline at end of file