8 changed files with 573 additions and 0 deletions
@ -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<ActionResult<List<ShopItem>>> GetAllShopItems() |
||||
|
{ |
||||
|
return await service.GetAllShopItems(); |
||||
|
} |
||||
|
|
||||
|
[HttpGet("admin/{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<ShopItem?>> GetShopItemById(int id) |
||||
|
{ |
||||
|
return await service.GetShopItemById(id); |
||||
|
} |
||||
|
|
||||
|
[HttpPost("admin")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> CreateShopItem([FromBody] ShopItem item) |
||||
|
{ |
||||
|
return await service.CreateShopItem(item); |
||||
|
} |
||||
|
|
||||
|
[HttpPut("admin/{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> UpdateShopItem(int id, [FromBody] ShopItem item) |
||||
|
{ |
||||
|
item.Id = id; |
||||
|
return await service.UpdateShopItem(item); |
||||
|
} |
||||
|
|
||||
|
[HttpDelete("admin/{id}")] |
||||
|
[Authorize(Roles = "admin")] |
||||
|
public async Task<ActionResult<bool>> DeleteShopItem(int id) |
||||
|
{ |
||||
|
return await service.DeleteShopItem(id); |
||||
|
} |
||||
|
|
||||
|
// ============ 玩家商店 ============
|
||||
|
|
||||
|
[HttpGet] |
||||
|
[Authorize] |
||||
|
public async Task<ActionResult<ShopDto>> GetCharacterShop() |
||||
|
{ |
||||
|
var characterList = await service.GetCharacterShop(currentUserService.UserId); |
||||
|
return characterList; |
||||
|
} |
||||
|
|
||||
|
[HttpPost("buy")] |
||||
|
[Authorize] |
||||
|
public async Task<ActionResult<bool>> BuyItem([FromBody] ShopBuyDto dto) |
||||
|
{ |
||||
|
return await service.BuyItem(currentUserService.UserId, dto.ShopItemId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ShopBuyDto |
||||
|
{ |
||||
|
public int ShopItemId { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
using SqlSugar; |
||||
|
using System.ComponentModel; |
||||
|
|
||||
|
namespace Build_God_Api.DB |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 商店物品配置表 - 维护可售卖的商品库
|
||||
|
/// </summary>
|
||||
|
public class ShopItem : BaseEntity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 物品类型
|
||||
|
/// </summary>
|
||||
|
public BagItemType ItemType { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 物品ID (装备模板ID/丹药ID/垃圾ID)
|
||||
|
/// </summary>
|
||||
|
public int ItemId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 灵石价格
|
||||
|
/// </summary>
|
||||
|
public int Price { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 每日限购次数(0表示不限购)
|
||||
|
/// </summary>
|
||||
|
public int DailyLimit { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 是否启用
|
||||
|
/// </summary>
|
||||
|
public bool IsActive { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 排序权重(越大越优先出现)
|
||||
|
/// </summary>
|
||||
|
public int SortOrder { get; set; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 玩家商店状态 - 记录每个玩家的商店数据
|
||||
|
/// </summary>
|
||||
|
public class CharacterShop : BaseEntity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 角色ID
|
||||
|
/// </summary>
|
||||
|
public int CharacterId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 上次刷新时间
|
||||
|
/// </summary>
|
||||
|
public DateTime LastRefreshTime { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 当前商品列表(JSON)- 存储当前刷出来的商品及库存
|
||||
|
/// 格式: [{"ShopItemId":1,"Stock":5},{"ShopItemId":2,"Stock":3}]
|
||||
|
/// </summary>
|
||||
|
public string ItemsJson { get; set; } = "[]"; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 玩家商店购买记录 - 记录每日购买次数
|
||||
|
/// </summary>
|
||||
|
public class CharacterShopPurchaseLog : BaseEntity |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 角色ID
|
||||
|
/// </summary>
|
||||
|
public int CharacterId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 商店物品配置ID
|
||||
|
/// </summary>
|
||||
|
public int ShopItemId { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 当日购买次数
|
||||
|
/// </summary>
|
||||
|
public int PurchasedCount { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 记录日期(yyyy-MM-dd)
|
||||
|
/// </summary>
|
||||
|
public string PurchaseDate { get; set; } = string.Empty; |
||||
|
} |
||||
|
} |
||||
@ -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<List<ShopItem>> GetAllShopItems(); |
||||
|
Task<ShopItem?> GetShopItemById(int id); |
||||
|
Task<bool> CreateShopItem(ShopItem item); |
||||
|
Task<bool> UpdateShopItem(ShopItem item); |
||||
|
Task<bool> DeleteShopItem(int id); |
||||
|
|
||||
|
// 玩家商店
|
||||
|
Task<ShopDto> GetCharacterShop(int characterId); |
||||
|
Task<bool> 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<ShopItemDisplayDto> 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<List<ShopItem>> GetAllShopItems() |
||||
|
{ |
||||
|
return await db.Queryable<ShopItem>() |
||||
|
.OrderBy(x => x.SortOrder, OrderByType.Desc) |
||||
|
.ToListAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<ShopItem?> GetShopItemById(int id) |
||||
|
{ |
||||
|
return await db.Queryable<ShopItem>().FirstAsync(x => x.Id == id); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> CreateShopItem(ShopItem item) |
||||
|
{ |
||||
|
await db.Insertable(item).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> 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<bool> DeleteShopItem(int id) |
||||
|
{ |
||||
|
await db.Deleteable<ShopItem>().Where(x => x.Id == id).ExecuteCommandAsync(); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// ============ 玩家商店 ============
|
||||
|
|
||||
|
public async Task<ShopDto> GetCharacterShop(int characterId) |
||||
|
{ |
||||
|
var characterShop = await db.Queryable<CharacterShop>() |
||||
|
.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<bool> BuyItem(int characterId, int shopItemId) |
||||
|
{ |
||||
|
var character = await db.Queryable<Character>().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<CharacterShop>() |
||||
|
.FirstAsync(x => x.CharacterId == characterId) |
||||
|
?? throw new Exception("玩家商店数据不存在"); |
||||
|
|
||||
|
// 检查库存
|
||||
|
var currentItems = JsonSerializer.Deserialize<List<ShopCurrentItem>>(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<CharacterShopPurchaseLog>() |
||||
|
.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<CharacterBag>() |
||||
|
.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<CharacterShop> RefreshShop(int characterId) |
||||
|
{ |
||||
|
var now = DateTime.Now; |
||||
|
|
||||
|
// 获取所有启用的商品,按权重排序后随机抽取
|
||||
|
var allItems = await db.Queryable<ShopItem>() |
||||
|
.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<ShopCurrentItem>(); |
||||
|
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<CharacterShop>() |
||||
|
.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<ShopDto> BuildShopDto(int characterId, CharacterShop characterShop) |
||||
|
{ |
||||
|
var currentItems = JsonSerializer.Deserialize<List<ShopCurrentItem>>(characterShop.ItemsJson) ?? new(); |
||||
|
var today = DateTime.Now.ToString("yyyy-MM-dd"); |
||||
|
|
||||
|
var result = new ShopDto |
||||
|
{ |
||||
|
CharacterId = characterId, |
||||
|
LastRefreshTime = characterShop.LastRefreshTime, |
||||
|
Items = new List<ShopItemDisplayDto>() |
||||
|
}; |
||||
|
|
||||
|
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<EquipmentTemplate>().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<Pill>().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<Scrap>().FirstAsync(x => x.Id == shopItem.ItemId); |
||||
|
display.ItemName = scrap?.Name; |
||||
|
display.Icon = scrap?.Icon; |
||||
|
} |
||||
|
|
||||
|
// 获取今日购买次数
|
||||
|
if (shopItem.DailyLimit > 0) |
||||
|
{ |
||||
|
var purchaseLog = await db.Queryable<CharacterShopPurchaseLog>() |
||||
|
.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<EquipmentTemplate>() |
||||
|
.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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue