feat: update shop and lottery management rules
This commit is contained in:
@@ -46,10 +46,12 @@ async def ensure_runtime_schema():
|
||||
item_key VARCHAR(50) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
cost INT NOT NULL DEFAULT 0,
|
||||
stock INT NOT NULL DEFAULT -1,
|
||||
"desc" VARCHAR(500) NULL,
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
UNIQUE(chat_id, item_key)
|
||||
);
|
||||
ALTER TABLE shop_items ADD COLUMN IF NOT EXISTS stock INT NOT NULL DEFAULT -1;
|
||||
CREATE INDEX IF NOT EXISTS idx_shop_items_chat_enabled ON shop_items(chat_id, enabled);
|
||||
CREATE TABLE IF NOT EXISTS shop_transactions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
@@ -146,6 +146,7 @@ class ShopItem(TimestampMixin, Model):
|
||||
item_key = fields.CharField(max_length=50)
|
||||
name = fields.CharField(max_length=255)
|
||||
cost = fields.IntField(default=0)
|
||||
stock = fields.IntField(default=-1) # -1 表示不限库存
|
||||
desc = fields.CharField(max_length=500, null=True)
|
||||
enabled = fields.BooleanField(default=True)
|
||||
|
||||
|
||||
+30
-71
@@ -25,8 +25,10 @@ const spamTesting = ref(false);
|
||||
const spamTestResult = ref<Record<string, any> | null>(null);
|
||||
const ruleForm = reactive({ type: 'keyword', pattern: '', score: 75 });
|
||||
const ruleSaving = ref(false);
|
||||
const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true });
|
||||
const shopForm = reactive({ item_key: '', name: '', cost: 0, stock: -1, desc: '', enabled: true });
|
||||
const shopDialogOpen = ref(false);
|
||||
const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'active' });
|
||||
const lotteryDialogOpen = ref(false);
|
||||
const activePage = ref<'dashboard' | 'shop' | 'lottery' | 'bans' | 'rules'>('dashboard');
|
||||
const activeTab = reactive({ shop: 'items', lottery: 'events' });
|
||||
const toast = ref('');
|
||||
@@ -248,12 +250,14 @@ async function saveShopItem() {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...shopForm, chat_id: selectedChatId.value || 0 })
|
||||
});
|
||||
Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true });
|
||||
Object.assign(shopForm, { item_key: '', name: '', cost: 0, stock: -1, desc: '', enabled: true });
|
||||
shopDialogOpen.value = false;
|
||||
await loadShopItems();
|
||||
} catch (e: any) { showToast(`保存商品失败:${e.message}`); }
|
||||
finally { shopSaving.value = false; }
|
||||
}
|
||||
async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); }
|
||||
function newShopItem() { Object.assign(shopForm, { item_key: '', name: '', cost: 0, stock: -1, desc: '', enabled: true }); shopDialogOpen.value = true; }
|
||||
async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); shopDialogOpen.value = true; }
|
||||
async function deleteShopItem(item: ShopItem) {
|
||||
if (item._loading) return;
|
||||
if (!await askConfirm('删除商品', `确认删除商品「${item.name}」?交易记录会保留。`, { confirmText: '删除', danger: true })) return;
|
||||
@@ -268,7 +272,8 @@ async function loadLotteries() {
|
||||
try { lotteries.value = (await fetchJson(`/api/lotteries${selectedQuery.value}`)).map((x: LotteryItem) => ({ ...x, _loading: false })); } catch {}
|
||||
}
|
||||
function resetLotteryForm() { Object.assign(lotteryForm, { id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'active' }); }
|
||||
function editLottery(item: LotteryItem) { Object.assign(lotteryForm, { ...item, id: item.id || 0 }); }
|
||||
function newLottery() { resetLotteryForm(); lotteryDialogOpen.value = true; }
|
||||
function editLottery(item: LotteryItem) { if (Number(item.participants_count || 0) > 0) return showToast('已有用户参与,不能编辑,仅可开奖或关闭'); Object.assign(lotteryForm, { ...item, id: item.id || 0 }); lotteryDialogOpen.value = true; }
|
||||
async function saveLottery() {
|
||||
if (!lotteryForm.title || !lotteryForm.prize) return showToast('请填写抽奖标题和奖品');
|
||||
if (lotteryForm.condition_type === 'points' && Number(lotteryForm.condition_value || 0) <= 0) return showToast('请填写积分门槛');
|
||||
@@ -291,6 +296,7 @@ async function saveLottery() {
|
||||
await fetchJson(url, { method: lotteryForm.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
showToast(lotteryForm.id ? '抽奖已更新' : '抽奖已创建');
|
||||
resetLotteryForm();
|
||||
lotteryDialogOpen.value = false;
|
||||
await loadLotteries();
|
||||
} catch (e: any) { showToast(`保存抽奖失败:${e.message}`); }
|
||||
finally { lotterySaving.value = false; }
|
||||
@@ -677,72 +683,18 @@ onBeforeUnmount(() => {
|
||||
<button v-if="chatContextMenu.item && !chatContextMenu.item.deleted && chatContextMenu.item.user_id" class="warning" @click="contextDeleteMessage(true)">⛔ 删除并封禁</button>
|
||||
</div>
|
||||
|
||||
<section v-show="activePage === 'shop'" class="tab-page manager-layout">
|
||||
<aside class="editor-panel"><div class="section-title"><span>商品编辑</span><small>{{ shopForm.item_key ? '编辑中' : '新建' }}</small></div><label>商品编号<SInput v-model="shopForm.item_key" placeholder="如 1 / vip_card" /></label><label>商品名称<SInput v-model="shopForm.name" placeholder="补签卡" /></label><label>积分价格<SInput v-model="shopForm.cost" placeholder="50" /></label><label>说明<SInput v-model="shopForm.desc" placeholder="展示在 /shop 内" /></label><label class="switch-line"><input v-model="shopForm.enabled" type="checkbox" /> 上架销售</label><div class="editor-actions"><SButton size="xs" color="primary" :loading="shopSaving" :disabled="shopSaving" @click="saveShopItem">保存商品</SButton><SButton size="xs" variant="outline" @click="Object.assign(shopForm, { item_key: '', name: '', cost: 0, desc: '', enabled: true })">新建</SButton></div></aside>
|
||||
<section class="data-panel"><div class="tabbar"><button :class="{active: activeTab.shop === 'items'}" @click="activeTab.shop='items'">商品列表</button><button :class="{active: activeTab.shop === 'tx'}" @click="activeTab.shop='tx'; loadShopTransactions()">交易记录</button></div><div v-if="activeTab.shop === 'items'" class="data-table"><div class="table-head"><span>编号/商品</span><span>价格</span><span>状态</span><span>操作</span></div><div v-for="item in shopItems" :key="item.item_key" class="table-row"><span><b>{{ item.item_key }}</b><em>{{ item.name }}</em><small>{{ item.desc || '无说明' }}</small></span><span>{{ item.cost }} 分</span><span><STag size="xs" :color="item.enabled ? 'success' : 'secondary'" variant="soft">{{ item.enabled ? '上架' : '下架' }}</STag></span><span class="row-actions"><SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton><SButton size="xs" variant="soft" color="destructive" :loading="item._loading" :disabled="item._loading" @click="deleteShopItem(item)">删除</SButton></span></div></div><div v-else class="data-table tx-table"><div class="table-head"><span>用户</span><span>商品</span><span>积分</span><span>时间</span></div><div v-for="tx in shopTransactions" :key="tx.id" class="table-row"><span>@{{ tx.username || tx.user_id }}</span><span>{{ tx.item_name }}</span><span>{{ tx.cost }} 分</span><span>{{ formatTime(tx.created_at) }}</span></div></div></section>
|
||||
<section v-show="activePage === 'shop'" class="tab-page manager-layout single-panel-page">
|
||||
<SCard class="panel data-panel" title="🛒 商城管理" :description="`${shopItems.length} 个商品`">
|
||||
<template #extra><SButton size="xs" color="primary" @click="newShopItem">新增商品</SButton></template>
|
||||
<div class="tabbar"><button :class="{active: activeTab.shop === 'items'}" @click="activeTab.shop='items'">商品列表</button><button :class="{active: activeTab.shop === 'tx'}" @click="activeTab.shop='tx'; loadShopTransactions()">交易记录</button></div>
|
||||
<div v-if="activeTab.shop === 'items'" class="data-table shop-table"><div class="table-head"><span>编号/商品</span><span>价格</span><span>库存</span><span>状态</span><span>操作</span></div><div v-for="item in shopItems" :key="item.item_key" class="table-row"><span><b>{{ item.item_key }}</b><em>{{ item.name }}</em><small>{{ item.desc || '无说明' }}</small></span><span>{{ item.cost }} 分</span><span>{{ Number(item.stock ?? -1) < 0 ? '不限' : item.stock }}</span><span><STag size="xs" :color="item.enabled ? 'success' : 'secondary'" variant="soft">{{ item.enabled ? '上架' : '下架' }}</STag></span><span class="row-actions"><SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton><SButton size="xs" variant="soft" color="destructive" :loading="item._loading" :disabled="item._loading" @click="deleteShopItem(item)">删除</SButton></span></div></div>
|
||||
<div v-else class="data-table tx-table"><div class="table-head"><span>用户</span><span>商品</span><span>积分</span><span>时间</span></div><div v-for="tx in shopTransactions" :key="tx.id" class="table-row"><span>@{{ tx.username || tx.user_id }}</span><span>{{ tx.item_name }}</span><span>{{ tx.cost }} 分</span><span>{{ formatTime(tx.created_at) }}</span></div></div>
|
||||
</SCard>
|
||||
</section>
|
||||
|
||||
<section v-show="activePage === 'lottery'" class="tab-page manager-layout lottery-page">
|
||||
<aside class="editor-panel lottery-editor">
|
||||
<div class="section-title"><span>{{ lotteryEditorTitle }}</span><small>{{ lotteryConditionSummary }} · {{ lotteryDrawSummary }}</small></div>
|
||||
|
||||
<label>抽奖标题<SInput v-model="lotteryForm.title" placeholder="如 月卡福利抽奖" /></label>
|
||||
<label>奖品内容<SInput v-model="lotteryForm.prize" placeholder="月卡一张 / 邀请码一个" /></label>
|
||||
<label>活动说明<SInput v-model="lotteryForm.description" placeholder="可选,显示在抽奖卡片中" /></label>
|
||||
|
||||
<div class="form-block">
|
||||
<span class="field-label">参与条件</span>
|
||||
<div class="seg-buttons">
|
||||
<SButton v-for="option in conditionOptions" :key="option.value" size="sm" :variant="lotteryForm.condition_type === option.value ? 'solid' : 'outline'" :color="lotteryForm.condition_type === option.value ? 'primary' : 'secondary'" @click="selectLotteryCondition(option.value)">
|
||||
{{ option.label }}
|
||||
</SButton>
|
||||
</div>
|
||||
<p class="field-help">{{ conditionOptions.find(o => o.value === lotteryForm.condition_type)?.desc }}</p>
|
||||
<label v-if="lotteryForm.condition_type === 'points'" class="inline-field">最低积分<SInput v-model="lotteryForm.condition_value" type="number" placeholder="100" /></label>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<span class="field-label">开奖方式</span>
|
||||
<div class="seg-buttons">
|
||||
<SButton v-for="option in drawOptions" :key="option.value" size="sm" :variant="lotteryForm.draw_type === option.value ? 'solid' : 'outline'" :color="lotteryForm.draw_type === option.value ? 'primary' : 'secondary'" @click="selectLotteryDraw(option.value)">
|
||||
{{ option.label }}
|
||||
</SButton>
|
||||
</div>
|
||||
<p class="field-help">{{ drawOptions.find(o => o.value === lotteryForm.draw_type)?.desc }}</p>
|
||||
<label v-if="lotteryForm.draw_type === 'people'" class="inline-field">开奖人数<SInput v-model="lotteryForm.end_value" type="number" placeholder="3" /></label>
|
||||
<div v-else class="time-picker-block">
|
||||
<label class="inline-field">开奖时间<input v-model="lotteryForm.draw_at" class="form-input native-time" type="datetime-local" /></label>
|
||||
<div class="quick-times">
|
||||
<SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(10)">10 分钟后</SButton>
|
||||
<SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(30)">30 分钟后</SButton>
|
||||
<SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(60)">1 小时后</SButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-block">
|
||||
<span class="field-label">发布状态</span>
|
||||
<div class="seg-buttons">
|
||||
<SButton v-for="option in statusOptions" :key="option.value" size="sm" :variant="lotteryForm.status === option.value ? 'solid' : 'outline'" :color="lotteryForm.status === option.value ? 'success' : 'secondary'" @click="lotteryForm.status = option.value">
|
||||
{{ option.label }}
|
||||
</SButton>
|
||||
</div>
|
||||
<p class="field-help">新建建议直接启用;草稿仅保存后台配置。</p>
|
||||
</div>
|
||||
|
||||
<div class="form-preview">
|
||||
<strong>{{ lotteryForm.title || '抽奖标题' }}</strong>
|
||||
<span>{{ lotteryForm.prize || '奖品内容' }}</span>
|
||||
<small>{{ lotteryConditionSummary }} / {{ lotteryDrawSummary }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<SButtonLoading color="primary" :loading="lotterySaving" :disabled="lotterySaving" @click="saveLottery">{{ lotteryForm.id ? '保存修改' : '创建抽奖' }}</SButtonLoading>
|
||||
<SButton variant="outline" :disabled="lotterySaving" @click="resetLotteryForm">清空</SButton>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section v-show="activePage === 'lottery'" class="tab-page manager-layout single-panel-page lottery-page">
|
||||
<SCard class="panel data-panel" title="🎁 抽奖项目" :description="`${lotteries.length} 个活动`">
|
||||
<template #extra><SButton size="xs" color="primary" @click="newLottery">新增抽奖</SButton></template>
|
||||
<div class="table-list lottery-list">
|
||||
<div v-for="item in lotteries" :key="item.id" class="data-row lottery-row">
|
||||
<div class="data-main">
|
||||
@@ -751,13 +703,13 @@ onBeforeUnmount(() => {
|
||||
<STag size="xs" :color="item.status === 'active' ? 'success' : item.status === 'drawn' ? 'primary' : item.status === 'cancelled' ? 'destructive' : 'secondary'" variant="soft">{{ item.status }}</STag>
|
||||
</div>
|
||||
<div class="row-desc">奖品:{{ item.prize }} · 条件:{{ item.condition_type }}{{ item.condition_value ? ':' + item.condition_value : '' }}</div>
|
||||
<div class="row-desc">开奖:{{ item.draw_type === 'time' ? formatTime(item.draw_at) : `满 ${item.max_participants || item.end_value || '-'} 人` }}</div>
|
||||
<div class="row-desc">开奖:{{ item.draw_type === 'time' ? formatTime(item.draw_at) : `满 ${item.max_participants || item.end_value || '-'} 人` }} · 参与:{{ item.participants_count || 0 }} 人</div>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<SButton size="xs" variant="outline" @click="editLottery(item)">编辑</SButton>
|
||||
<SButton size="xs" variant="outline" :disabled="Number(item.participants_count || 0) > 0" @click="editLottery(item)">编辑</SButton>
|
||||
<SButton size="xs" variant="outline" @click="showParticipants(item)">参与</SButton>
|
||||
<SButton size="xs" variant="outline" color="primary" :loading="item._loading" :disabled="item._loading || item.status !== 'active'" @click="drawLottery(item)">开奖</SButton>
|
||||
<SButton size="xs" variant="outline" color="warning" :loading="item._loading" :disabled="item._loading || item.status !== 'active'" @click="cancelLottery(item)">取消</SButton>
|
||||
<SButton size="xs" variant="outline" color="warning" :loading="item._loading" :disabled="item._loading || item.status !== 'active'" @click="cancelLottery(item)">关闭</SButton>
|
||||
<SButton size="xs" variant="link" color="destructive" :loading="item._loading" :disabled="item._loading" @click="deleteLottery(item)">删除</SButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -776,13 +728,20 @@ onBeforeUnmount(() => {
|
||||
<SButton type="submit" color="primary" :loading="sending" :disabled="sending || !newMessage.trim()">发送</SButton>
|
||||
</form>
|
||||
|
||||
<div v-if="shopDialogOpen" class="confirm-mask" @click.self="shopDialogOpen=false">
|
||||
<div class="confirm-card editor-dialog"><h3>{{ shopForm.item_key ? '编辑商品' : '新增商品' }}</h3><p>库存填 -1 表示不限库存,0 表示售罄。</p><label>商品编号<SInput v-model="shopForm.item_key" placeholder="如 1 / vip_card" /></label><label>商品名称<SInput v-model="shopForm.name" placeholder="补签卡" /></label><label>积分价格<SInput v-model="shopForm.cost" type="number" placeholder="50" /></label><label>库存<SInput v-model="shopForm.stock" type="number" placeholder="-1 不限库存" /></label><label>说明<SInput v-model="shopForm.desc" placeholder="展示在 /shop 内" /></label><label class="switch-line"><input v-model="shopForm.enabled" type="checkbox" /> 上架销售</label><div class="confirm-actions"><SButton size="xs" variant="outline" :disabled="shopSaving" @click="shopDialogOpen=false">取消</SButton><SButtonLoading size="xs" color="primary" :loading="shopSaving" :disabled="shopSaving" @click="saveShopItem">保存</SButtonLoading></div></div>
|
||||
</div>
|
||||
<div v-if="lotteryDialogOpen" class="confirm-mask" @click.self="lotteryDialogOpen=false">
|
||||
<div class="confirm-card editor-dialog lottery-dialog"><h3>{{ lotteryEditorTitle }}</h3><p>{{ lotteryConditionSummary }} · {{ lotteryDrawSummary }}</p><label>抽奖标题<SInput v-model="lotteryForm.title" placeholder="如 月卡福利抽奖" /></label><label>奖品内容<SInput v-model="lotteryForm.prize" placeholder="月卡一张 / 邀请码" /></label><label>说明<SInput v-model="lotteryForm.description" placeholder="展示给参与用户" /></label><div class="segmented"><button v-for="option in conditionOptions" :key="option.value" :class="{active: lotteryForm.condition_type === option.value}" @click="selectLotteryCondition(option.value)"><b>{{ option.label }}</b><small>{{ option.desc }}</small></button></div><label v-if="lotteryForm.condition_type === 'points'">积分门槛<SInput v-model="lotteryForm.condition_value" type="number" placeholder="100" /></label><div class="segmented"><button v-for="option in drawOptions" :key="option.value" :class="{active: lotteryForm.draw_type === option.value}" @click="selectLotteryDraw(option.value)"><b>{{ option.label }}</b><small>{{ option.desc }}</small></button></div><label v-if="lotteryForm.draw_type === 'people'">开奖人数<SInput v-model="lotteryForm.end_value" type="number" placeholder="3" /></label><div v-else class="datetime-block"><label>开奖时间<input v-model="lotteryForm.draw_at" class="form-input" type="datetime-local" /></label><div class="quick-time"><SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(10)">10 分钟</SButton><SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(30)">30 分钟</SButton><SButton size="xs" variant="outline" @click="setLotteryDrawAtMinutes(60)">1 小时</SButton></div></div><div class="segmented small"><button v-for="option in statusOptions" :key="option.value" :class="{active: lotteryForm.status === option.value}" @click="lotteryForm.status = option.value"><b>{{ option.label }}</b></button></div><div class="confirm-actions"><SButton size="xs" variant="outline" :disabled="lotterySaving" @click="lotteryDialogOpen=false">取消</SButton><SButtonLoading size="xs" color="primary" :loading="lotterySaving" :disabled="lotterySaving" @click="saveLottery">保存</SButtonLoading></div></div>
|
||||
</div>
|
||||
|
||||
<div v-if="toast" class="toast">{{ toast }}</div>
|
||||
<div v-if="confirmState.open" class="confirm-mask" @click.self="closeConfirm(false)">
|
||||
<div class="confirm-card">
|
||||
<h3>{{ confirmState.title }}</h3>
|
||||
<p>{{ confirmState.message }}</p>
|
||||
<div class="confirm-actions">
|
||||
<SButton size="xs" variant="outline" @click="closeConfirm(false)">取消</SButton>
|
||||
<SButton size="xs" variant="outline" @click="closeConfirm(false)">关闭</SButton>
|
||||
<SButton size="xs" :color="confirmState.danger ? 'destructive' : 'primary'" @click="closeConfirm(true)">{{ confirmState.confirmText }}</SButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -329,3 +329,14 @@ body { background: #f3f6fb; }
|
||||
.chat-context-menu button.warning:hover { background: #fff7ed; color: #ea580c; }
|
||||
.chat-item { user-select: text; }
|
||||
.chat-item:hover { outline: 1px solid rgba(59,130,246,.18); }
|
||||
|
||||
|
||||
/* Dialog based shop/lottery editing */
|
||||
.single-panel-page { display: block; }
|
||||
.single-panel-page .data-panel { min-height: 0; }
|
||||
.editor-dialog { width: min(620px, calc(100vw - 28px)); display: grid; gap: 12px; }
|
||||
.editor-dialog h3 { margin: 0; }
|
||||
.editor-dialog p { margin: -4px 0 2px; color: #64748b; font-size: 13px; }
|
||||
.lottery-dialog { max-height: min(82vh, 760px); overflow-y: auto; }
|
||||
.shop-table .table-head, .shop-table .table-row { grid-template-columns: minmax(0, 1.5fr) 90px 80px 80px 150px; }
|
||||
@media (max-width: 780px) { .shop-table .table-head, .shop-table .table-row { grid-template-columns: 1fr; } }
|
||||
|
||||
@@ -124,18 +124,19 @@ async def list_shop_items(chat_id: int, include_disabled: bool = False) -> list[
|
||||
if not rows and chat_id != 0:
|
||||
rows = await ShopItem.filter(chat_id=0).order_by("item_key").all()
|
||||
if not rows:
|
||||
return [{"item_key": k, "name": v["name"], "cost": v["cost"], "desc": v["desc"], "enabled": True} for k, v in SHOP_ITEMS.items()]
|
||||
return [{"item_key": k, "name": v["name"], "cost": v["cost"], "stock": -1, "desc": v["desc"], "enabled": True} for k, v in SHOP_ITEMS.items()]
|
||||
result = []
|
||||
for r in rows:
|
||||
if include_disabled or r.enabled:
|
||||
result.append({"id": r.id, "chat_id": r.chat_id, "item_key": r.item_key, "name": r.name, "cost": r.cost, "desc": r.desc, "enabled": r.enabled})
|
||||
result.append({"id": r.id, "chat_id": r.chat_id, "item_key": r.item_key, "name": r.name, "cost": r.cost, "stock": r.stock, "desc": r.desc, "enabled": r.enabled})
|
||||
return result
|
||||
|
||||
|
||||
async def upsert_shop_item(chat_id: int, item_key: str, name: str, cost: int, desc: str = "", enabled: bool = True) -> ShopItem:
|
||||
item, _ = await ShopItem.get_or_create(chat_id=chat_id, item_key=item_key, defaults={"name": name, "cost": cost, "desc": desc, "enabled": enabled})
|
||||
async def upsert_shop_item(chat_id: int, item_key: str, name: str, cost: int, desc: str = "", enabled: bool = True, stock: int = -1) -> ShopItem:
|
||||
item, _ = await ShopItem.get_or_create(chat_id=chat_id, item_key=item_key, defaults={"name": name, "cost": cost, "desc": desc, "enabled": enabled, "stock": stock})
|
||||
item.name = name
|
||||
item.cost = cost
|
||||
item.stock = stock
|
||||
item.desc = desc
|
||||
item.enabled = enabled
|
||||
await item.save()
|
||||
@@ -157,8 +158,15 @@ async def buy_shop_item(chat_id: int, user_id: int, item_id: str, username: str
|
||||
item = next((x for x in items if str(x.get("item_key")) == str(item_id) and x.get("enabled", True)), None)
|
||||
if not item:
|
||||
return {"ok": False, "reason": "商品不存在"}
|
||||
db_item = await ShopItem.filter(chat_id=int(item.get("chat_id") or chat_id), item_key=str(item.get("item_key"))).first()
|
||||
if db_item and db_item.stock == 0:
|
||||
return {"ok": False, "reason": "库存不足", "item": item}
|
||||
if not await deduct_points(chat_id, user_id, int(item["cost"])):
|
||||
return {"ok": False, "reason": "积分不足", "item": item}
|
||||
if db_item and db_item.stock > 0:
|
||||
db_item.stock -= 1
|
||||
await db_item.save(update_fields=["stock", "updated_at"])
|
||||
item["stock"] = db_item.stock
|
||||
await ShopTransaction.create(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
|
||||
+9
-3
@@ -387,8 +387,9 @@ async def api_save_shop_item(request: Request):
|
||||
cost=int(data.get("cost") or 0),
|
||||
desc=str(data.get("desc") or ""),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
stock=int(data.get("stock", -1) if data.get("stock", -1) not in (None, "") else -1),
|
||||
)
|
||||
return JSONResponse(json_safe({"ok": True, "item": {"id": item.id, "chat_id": item.chat_id, "item_key": item.item_key, "name": item.name, "cost": item.cost, "desc": item.desc, "enabled": item.enabled}}))
|
||||
return JSONResponse(json_safe({"ok": True, "item": {"id": item.id, "chat_id": item.chat_id, "item_key": item.item_key, "name": item.name, "cost": item.cost, "stock": item.stock, "desc": item.desc, "enabled": item.enabled}}))
|
||||
|
||||
|
||||
@app.delete("/api/shop-items/{item_key}")
|
||||
@@ -407,8 +408,10 @@ async def api_lotteries(chat_id: int | None = None, limit: int = Query(default=1
|
||||
qs = LotteryEvent.all()
|
||||
if chat_id is not None:
|
||||
qs = qs.filter(chat_id=chat_id)
|
||||
rows = await qs.order_by("-created_at").limit(limit).values()
|
||||
return JSONResponse(json_safe(list(rows)))
|
||||
rows = list(await qs.order_by("-created_at").limit(limit).values())
|
||||
for row in rows:
|
||||
row["participants_count"] = await LotteryParticipant.filter(event_id=row["id"]).count()
|
||||
return JSONResponse(json_safe(rows))
|
||||
|
||||
|
||||
@app.post("/api/lotteries")
|
||||
@@ -443,6 +446,9 @@ async def api_update_lottery(event_id: int, request: Request):
|
||||
event = await LotteryEvent.filter(id=event_id).first()
|
||||
if not event:
|
||||
return JSONResponse({"ok": False, "error": "抽奖不存在"}, status_code=404)
|
||||
participant_count = await LotteryParticipant.filter(event_id=event_id).count()
|
||||
if participant_count > 0:
|
||||
return JSONResponse({"ok": False, "error": "已有用户参与,不能编辑抽奖;仅可强制开奖或关闭"}, status_code=409)
|
||||
data = await request.json()
|
||||
for key in ["title", "description", "prize", "condition_type", "draw_type", "end_type", "status"]:
|
||||
if key in data:
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
||||
<title>TG Spam Guard</title>
|
||||
<script type="module" crossorigin src="/static/assets/index-Orp-c1A_.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-BAKZPs4k.css">
|
||||
<script type="module" crossorigin src="/static/assets/index-BhOEYMlC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/assets/index-tp52gmZ4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
Reference in New Issue
Block a user