Schema: ItemPurchaseCatalogEntry (single table). Per-viewer quota tracked via existing ViewerEventCounter keyed by "item_purchase:<id>" with period JstPeriod.MonthKey when IsMonthlyReset else AllTime. Controller: - /info returns catalog + per-period rest (server-computed max(0, PurchaseLimit - counter)) + user_card_pack_ticket_list (every Items.Type==2 row joined to viewer count, zeros included — client unconditionally UpdateItemNum's each entry). - /purchase: sold_out check before currency check (no counter increment on currency failure), inline TryDebit covers RedEther/Crystal/Rupy/Item with post-state-total reward_list entry, grant via RewardGrantService. Request `rest` accepted but ignored (server counter is canonical). Importer mirrors PaymentItemImporter shape — idempotent find-or-create, seed-missing rows preserved. 3 entries from the prod capture. 486 tests pass (was 476; +10 item_purchase tests). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
40 lines
1.6 KiB
C#
40 lines
1.6 KiB
C#
using SVSim.Database.Common;
|
||
|
||
namespace SVSim.Database.Models;
|
||
|
||
/// <summary>
|
||
/// One row of the /item_purchase/info catalog — an exchange the user can perform N times per
|
||
/// period (monthly or lifetime) by spending <c>RequireItem*</c> to acquire <c>PurchaseItem*</c>.
|
||
/// PK = wire <c>purchase_id</c>.
|
||
/// <para>
|
||
/// Both sides reference <see cref="Enums.UserGoodsType"/>. Captures show the common shape is
|
||
/// currency-for-item (RedEther 5000 → Seer's Globe ×1) or item-for-item (Orb Shard ×5 →
|
||
/// Seer's Globe ×1). Per-viewer remaining quota lives in
|
||
/// <see cref="ViewerEventCounter"/> keyed by <c>"item_purchase:{Id}"</c>.
|
||
/// </para>
|
||
/// </summary>
|
||
public class ItemPurchaseCatalogEntry : BaseEntity<int>
|
||
{
|
||
public int RequireItemType { get; set; }
|
||
public long RequireItemId { get; set; }
|
||
public int RequireItemNum { get; set; }
|
||
|
||
public int PurchaseItemType { get; set; }
|
||
public long PurchaseItemId { get; set; }
|
||
public int PurchaseItemNum { get; set; }
|
||
|
||
/// <summary>
|
||
/// SystemText-ready display name. May be empty — the client falls back to a templated name
|
||
/// built from <c>UserGoods.getUserGoodsName + count</c> via SystemText key "Shop_0132".
|
||
/// </summary>
|
||
public string PurchaseName { get; set; } = string.Empty;
|
||
|
||
/// <summary>True → quota resets at the start of each JST month. False → lifetime quota.</summary>
|
||
public bool IsMonthlyReset { get; set; }
|
||
|
||
/// <summary>Per-period purchase cap. Wire <c>rest</c> = max(0, PurchaseLimit - counter).</summary>
|
||
public int PurchaseLimit { get; set; }
|
||
|
||
public bool IsEnabled { get; set; }
|
||
}
|