diff --git a/.tasks/2025-11-06_1_migrate-game-bot.md b/.tasks/2025-11-06_1_migrate-game-bot.md index 53d78cc..b19acd4 100644 --- a/.tasks/2025-11-06_1_migrate-game-bot.md +++ b/.tasks/2025-11-06_1_migrate-game-bot.md @@ -30,7 +30,7 @@ Yolo模式: Off - 暴露 get_user_name/url/points 与 adjust_user_points 接口供其他插件调用。 - 通过 asyncio.Lock 确保写入串行, 保存失败会记录错误日志。 -# 当前执行步骤:"3. 实施配置插件" +# 当前执行步骤:"4. 修复签到唯一约束" # 任务进度 @@ -67,4 +67,13 @@ Yolo模式: Off [2025-11-07_22:33:00] - 已修改: Plugins/WPSPointSystem->Plugins/WPSConfigSystem -- 原因: user_id相关的最优先表被注册在此处 \ No newline at end of file +- 原因: user_id相关的最优先表被注册在此处 +- 阻碍因素: 无 +- 状态: 成功 + +[2025-11-09_19:35:08] +- 已修改: `Plugins/WPSConfigSystem.py` +- 更改: 在记录每日签到时使用 INSERT ... ON CONFLICT(user_id) DO UPDATE,避免跨日签到触发唯一约束错误 +- 原因: 保障每日签到功能在重复执行时保持幂等性 +- 阻碍因素: 无 +- 状态: 成功 \ No newline at end of file diff --git a/.tasks/2025-11-08_1_store-system.md b/.tasks/2025-11-08_1_store-system.md index f45aec3..676e0b5 100644 --- a/.tasks/2025-11-08_1_store-system.md +++ b/.tasks/2025-11-08_1_store-system.md @@ -45,7 +45,7 @@ WPS Bot 插件体系,现有 `WPSConfigAPI` 提供积分管理与签到积分 - 重新整理依赖关系与命令注册,确保未注册商品或物品未找到时返回明确错误提示(如“未找到商店商品”“背包未注册物品”)。 - 暴露额外接口:`list_registered_modes()`、`get_store_snapshot()` 等辅助方法供其他插件调试或扩展使用。 -# 当前执行步骤:"5. 重新规划命令拆分" +# 当前执行步骤:"10. 购买排序优化" 实施清单: 1. 重构 `WPSStoreSystem`:新增 `purchase_item`/`sell_item` 公共方法,并将 `callback` 限制为 `商店` 指令。 @@ -109,6 +109,36 @@ WPS Bot 插件体系,现有 `WPSConfigAPI` 提供积分管理与签到积分 - 原因:满足命令拆分要求,避免未注册商品时误返回帮助信息 - 阻碍因素:无 - 状态:未确认 +2025-11-09_19:18:39 +- 已修改:`Plugins/WPSStoreSystem.py` +- 更改:玩家出售与系统商品展示统一使用物品名称,扩展 `PlayerListing` 并在加载时补充物品名 +- 原因:确保商店界面不再暴露物品ID,提升可读性 +- 阻碍因素:无 +- 状态:未确认 +2025-11-09_19:35:08 +- 已修改:`Plugins/WPSStoreSystem.py` `PWF/CoreModules/plugin_interface.py` +- 更改:出售指令支持 `<数量> <单价>` 参数,调整逻辑使用玩家自定 single price,并在表注册时自动补齐缺失列 +- 原因:允许运营自定售价并避免旧表缺列导致的运行错误 +- 阻碍因素:无 +- 状态:成功 +2025-11-09_19:45:00 +- 已修改:`Plugins/WPSStoreSystem.py` +- 更改:购买流程统一收集匹配来源,按价格升序排序;同价时优先玩家,再按系统、常驻顺序处理 +- 原因:满足按价格优先购买的需求,确保同价时玩家商品优先售出 +- 阻碍因素:无 +- 状态:进行中 +2025-11-09_19:50:00 +- 已修改:`Plugins/WPSAlchemyGame.py` +- 更改:炼金插件依赖商店系统,在注册炉灰物品后同步为整点刷新候选,单价 8 分、限购 999 +- 原因:补充基础货源,便于验证按价格排序的购买流程 +- 阻碍因素:无 +- 状态:成功 +2025-11-09_21:22:44 +- 已修改:`Plugins/WPSStoreSystem.py` +- 更改:玩家出售展示改为调用配置接口获取昵称,避免暴露纯数字ID +- 原因:提升商店玩家出售区的可读性与一致性 +- 阻碍因素:无 +- 状态:未确认 # 最终审查 (待补充) diff --git a/PWF b/PWF index 4d3d841..7deef90 160000 --- a/PWF +++ b/PWF @@ -1 +1 @@ -Subproject commit 4d3d841ddac30a060c333cb248d629437234d635 +Subproject commit 7deef9092a91225a7948352862bd7d9967f2704f diff --git a/Plugins/WPSAlchemyGame.py b/Plugins/WPSAlchemyGame.py index bc20045..87bbc96 100644 --- a/Plugins/WPSAlchemyGame.py +++ b/Plugins/WPSAlchemyGame.py @@ -15,6 +15,7 @@ from .WPSBackpackSystem import ( WPSBackpackSystem, ) from .WPSConfigSystem import WPSConfigAPI +from .WPSStoreSystem import WPSStoreSystem from .WPSFortuneSystem import WPSFortuneSystem @@ -40,7 +41,7 @@ class WPSAlchemyGame(WPSAPI): ASH_ITEM_NAME = "炉灰" SLAG_ITEM_ID = "alchemy_slag" SLAG_ITEM_NAME = "炉渣" - MAX_BATCH_TIMES = 9999 + MAX_BATCH_TIMES = 99 _PHASE_TABLE: List[Tuple[float, float, str, str]] = [ (0.1481481481, 0.0, "爆炸", "💥 炼金反噬,积分化为飞灰……"), @@ -58,7 +59,7 @@ class WPSAlchemyGame(WPSAPI): @override def dependencies(self) -> List[type]: - return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem] + return [WPSAPI, WPSBackpackSystem, WPSConfigAPI, WPSFortuneSystem, WPSStoreSystem] @override def wake_up(self) -> None: @@ -83,6 +84,19 @@ class WPSAlchemyGame(WPSAPI): "Warning", f"{ConsoleFrontColor.YELLOW}注册炉灰物品时出现问题: {exc}{ConsoleFrontColor.RESET}", ) + else: + try: + store_system = Architecture.Get(WPSStoreSystem) + store_system.register_mode( + item_id=self.ASH_ITEM_ID, + price=8, + limit_amount=999, + ) + except Exception as exc: + logger.Log( + "Warning", + f"{ConsoleFrontColor.YELLOW}注册炉灰商店模式时出现问题: {exc}{ConsoleFrontColor.RESET}", + ) try: backpack.register_item( self.SLAG_ITEM_ID, @@ -180,7 +194,7 @@ class WPSAlchemyGame(WPSAPI): if points <= 0: return "❌ 投入积分必须大于 0" config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) - current_points = config_api.get_user_points(chat_id, user_id) + current_points = config_api.get_user_points(user_id) if current_points < points: return f"❌ 积分不足,需要 {points} 分,当前仅有 {current_points} 分" @@ -205,7 +219,7 @@ class WPSAlchemyGame(WPSAPI): backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) backpack.add_item(user_id, self.ASH_ITEM_ID, ash_reward) - final_points = config_api.get_user_points(chat_id, user_id) + final_points = config_api.get_user_points(user_id) extra_line = "" if ash_reward > 0: extra_line = ( diff --git a/Plugins/WPSConfigSystem.py b/Plugins/WPSConfigSystem.py index f05cd51..7a5224c 100644 --- a/Plugins/WPSConfigSystem.py +++ b/Plugins/WPSConfigSystem.py @@ -160,21 +160,21 @@ class WPSConfigAPI(WPSAPI): except (TypeError, ValueError): return 0 - def get_user_name(self, chat_id: int, user_id: int) -> Optional[str]: + def get_user_name(self, user_id: int) -> Optional[str]: record = self._get_user_record(user_id) if not record: return f"user_{user_id}" value = record.get("username") return str(value) if value else f"user_{user_id}" - def get_user_url(self, chat_id: int, user_id: int) -> Optional[str]: + def get_user_url(self, user_id: int) -> Optional[str]: record = self._get_user_record(user_id) if not record: return None value = record.get("userurl") return str(value) if value else None - def get_user_points(self, chat_id: int, user_id: int) -> int: + def get_user_points(self, user_id: int) -> int: record = self._get_user_record(user_id) if not record: return 0 @@ -224,7 +224,15 @@ class WPSCheckinAPI(WPSAPI): def _set_today_checkin_status(self, user_id: int) -> None: cursor = get_db().conn.cursor() - cursor.execute("INSERT INTO daily_checkin (user_id, checkin_date) VALUES (?, ?)", (user_id, datetime.now().strftime("%Y-%m-%d"))) + cursor.execute( + """ + INSERT INTO daily_checkin (user_id, checkin_date) + VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET + checkin_date = excluded.checkin_date + """, + (user_id, datetime.now().strftime("%Y-%m-%d")), + ) get_db().conn.commit() async def do_callback(self, message: str, chat_id: int, user_id: int) -> Optional[str]: @@ -259,7 +267,7 @@ class WPSCheckinAPI(WPSAPI): return f'''# 📅 Daily 命令帮助 - checkin: 签到 --- -- 当前分数: {wps_config_api.get_user_points(chat_id, user_id)} +- 当前分数: {wps_config_api.get_user_points(user_id)} - 今日签到状态: {"已签到" if self._get_today_checkin_status(user_id) else "未签到"} ''' diff --git a/Plugins/WPSStoreSystem.py b/Plugins/WPSStoreSystem.py index b40a418..21f2dec 100644 --- a/Plugins/WPSStoreSystem.py +++ b/Plugins/WPSStoreSystem.py @@ -59,6 +59,7 @@ class StoreEntry(BaseModel): class PlayerListing(BaseModel): user_id: int item_id: str + item_name: str price: int quantity: int created_at: datetime @@ -390,6 +391,7 @@ class WPSStoreSystem(WPSAPI): system_entries, permanent_entries = self._fetch_system_entries() player_listings = self._fetch_player_listings() return self._format_store_markdown( + chat_id=chat_id, user_id=user_id, system_entries=system_entries, permanent_entries=permanent_entries, @@ -413,25 +415,63 @@ class WPSStoreSystem(WPSAPI): system_entries, permanent_entries = self._fetch_system_entries() player_listings = self._fetch_player_listings() - target = self._resolve_system_entry(identifier, system_entries, permanent_entries) - if target: - return await self._purchase_system_entry( - entry=target, + matched_system_entries = self._resolve_system_entries( + identifier=identifier, + system_entries=system_entries, + permanent_entries=permanent_entries, + ) + matched_player_listings = await self._resolve_player_listings( + identifier=identifier, + listings=player_listings, + ) + + if not matched_system_entries and not matched_player_listings: + return "❌ 未找到匹配的商品,请确认名称或物品ID是否正确" + + candidates: list[tuple[int, int, str, StoreEntry | PlayerListing]] = [] + + for idx, listing in enumerate(matched_player_listings): + candidates.append((listing.price, 0, f"player-{idx}", listing)) + for idx, entry in enumerate(system_entries): + if entry in matched_system_entries["system"]: + candidates.append((entry.price, 1, f"system-{idx}", entry)) + for idx, entry in enumerate(permanent_entries): + if entry in matched_system_entries["permanent"]: + candidates.append((entry.price, 2, f"permanent-{idx}", entry)) + + if not candidates: + return "❌ 未找到匹配的商品,请确认名称或物品ID是否正确" + + candidates.sort(key=lambda item: (item[0], item[1], item[2])) + + for price, source_priority, _, payload in candidates: + if source_priority == 0: + listing = payload # type: ignore[assignment] + response = await self._purchase_player_listing( + listing=listing, + quantity=quantity, + chat_id=chat_id, + user_id=user_id, + ) + if not response.startswith("❌"): + return response + if "库存不足" in response: + continue + return response + entry = payload # type: ignore[assignment] + response = await self._purchase_system_entry( + entry=entry, quantity=quantity, chat_id=chat_id, user_id=user_id, ) + if not response.startswith("❌"): + return response + if "库存不足" in response: + continue + return response - listing = await self._resolve_player_listing(identifier, player_listings) - if listing: - return await self._purchase_player_listing( - listing=listing, - quantity=quantity, - chat_id=chat_id, - user_id=user_id, - ) - - return "❌ 未找到匹配的商品,请确认名称或物品ID是否正确" + return "❌ 所有匹配的商品已售罄或库存不足" async def sell_item( self, @@ -440,12 +480,15 @@ class WPSStoreSystem(WPSAPI): user_id: int, identifier: str, quantity: int, + price: int, ) -> str: identifier = identifier.strip() if not identifier: return "❌ 出售指令格式错误,请提供物品名称或ID" if quantity < 0: return "❌ 出售数量必须大于或等于0" + if price < 0: + return "❌ 出售单价必须是非负整数" backpack = Architecture.Get(WPSBackpackSystem) item_id, definition = self._resolve_item(identifier) if item_id is None: @@ -482,10 +525,6 @@ class WPSStoreSystem(WPSAPI): elif diff < 0: backpack.add_item(user_id, item_id, -diff) - try: - price = self._derive_player_price(item_id) - except ValueError as exc: - return f"❌ 当前物品 {definition.name} 未设置售价,无法出售({exc})" cursor = get_db().conn.cursor() cursor.execute( f""" @@ -505,10 +544,6 @@ class WPSStoreSystem(WPSAPI): if available_qty < quantity: return f"❌ 背包数量不足,当前拥有 {available_qty} 个 {definition.name}" - try: - price = self._derive_player_price(item_id) - except ValueError as exc: - return f"❌ 当前物品 {definition.name} 未设置售价,无法出售({exc})" backpack.set_item_quantity(user_id, item_id, available_qty - quantity) cursor = get_db().conn.cursor() @@ -532,7 +567,7 @@ class WPSStoreSystem(WPSAPI): return """# 🛒 商店指令帮助 - `商店`:查看当前系统商品与玩家出售列表 - `购买 <物品名称或ID> <数量>`:购买指定商品 -- `出售 <物品名称或ID> <数量>`:上架自己的出售物品(限一种) +- `出售 <物品名称或ID> <数量> <单价>`:上架或更新自己的出售物品(限一种) """ def _fetch_system_entries(self) -> Tuple[List[StoreEntry], List[StoreEntry]]: @@ -582,11 +617,19 @@ class WPSStoreSystem(WPSAPI): ) rows = cursor.fetchall() listings: List[PlayerListing] = [] + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) for row in rows: + item_id = row["item_id"] + try: + definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + item_name = definition.name + except Exception: + item_name = item_id listings.append( PlayerListing( user_id=row["user_id"], - item_id=row["item_id"], + item_id=item_id, + item_name=item_name, price=row["price"], quantity=row["quantity"], created_at=self._parse_datetime(row["created_at"]), @@ -598,12 +641,14 @@ class WPSStoreSystem(WPSAPI): def _format_store_markdown( self, *, + chat_id: int, user_id: int, system_entries: List[StoreEntry], permanent_entries: List[StoreEntry], player_listings: List[PlayerListing], ) -> str: lines: List[str] = ["# 🏬 每小时商店"] + config_api: WPSConfigAPI = Architecture.Get(WPSConfigAPI) if permanent_entries: lines.append("## ♾️ 常驻商品") for entry in permanent_entries: @@ -613,7 +658,7 @@ class WPSStoreSystem(WPSAPI): else f"限购 {entry.limit_amount}" ) lines.append( - f"- `{entry.item_id}` · {entry.display_name}|价格 {entry.price} 分|{limit_text}" + f"- {entry.display_name}|价格 {entry.price} 分|{limit_text}" ) if system_entries: lines.append("## ⏱️ 本时段商品") @@ -623,7 +668,7 @@ class WPSStoreSystem(WPSAPI): else: stock_text = f"剩余:{entry.remaining_amount}" lines.append( - f"- `{entry.item_id}` · {entry.display_name}|价格 {entry.price} 分|{stock_text}" + f"- {entry.display_name}|价格 {entry.price} 分|{stock_text}" ) if not permanent_entries and not system_entries: lines.append("> ⚠️ 当前没有可售的系统商品") @@ -631,9 +676,11 @@ class WPSStoreSystem(WPSAPI): lines.append("## 👥 玩家出售") if player_listings: for listing in player_listings: - owner = "你" if listing.user_id == user_id else f"玩家 {listing.user_id}" + owner = "你" + if listing.user_id != user_id: + owner = config_api.get_user_name(listing.user_id) lines.append( - f"- {owner}|`{listing.item_id}`|数量 {listing.quantity}|价格 {listing.price} 分" + f"- {owner}|{listing.item_name}|数量 {listing.quantity}|价格 {listing.price} 分" ) else: lines.append("> 当前暂无玩家出售信息") @@ -689,27 +736,32 @@ class WPSStoreSystem(WPSAPI): f"当前剩余积分:{new_points}" ) - async def _resolve_player_listing( + async def _resolve_player_listings( self, identifier: str, listings: List[PlayerListing], - ) -> Optional[PlayerListing]: + ) -> List[PlayerListing]: if not listings: - return None - backpack = Architecture.Get(WPSBackpackSystem) + return [] + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) identifier_lower = identifier.lower() + matches: List[PlayerListing] = [] for listing in listings: if listing.quantity <= 0: continue if listing.item_id.lower() == identifier_lower: - return listing + matches.append(listing) + continue + if listing.item_name.lower() == identifier_lower: + matches.append(listing) + continue try: definition = backpack._get_definition(listing.item_id) # type: ignore[attr-defined] except Exception: continue if definition.name.lower() == identifier_lower: - return listing - return None + matches.append(listing) + return matches async def _purchase_player_listing( self, @@ -772,19 +824,22 @@ class WPSStoreSystem(WPSAPI): f"当前剩余积分:{buyer_new_points}" ) - def _resolve_system_entry( + def _resolve_system_entries( self, identifier: str, system_entries: List[StoreEntry], permanent_entries: List[StoreEntry], - ) -> Optional[StoreEntry]: + ) -> Dict[str, List[StoreEntry]]: identifier_lower = identifier.lower() - for entry in permanent_entries + system_entries: - if entry.item_id.lower() == identifier_lower: - return entry - if entry.display_name.lower() == identifier_lower: - return entry - return None + matched_system: List[StoreEntry] = [] + matched_permanent: List[StoreEntry] = [] + for entry in permanent_entries: + if entry.item_id.lower() == identifier_lower or entry.display_name.lower() == identifier_lower: + matched_permanent.append(entry) + for entry in system_entries: + if entry.item_id.lower() == identifier_lower or entry.display_name.lower() == identifier_lower: + matched_system.append(entry) + return {"system": matched_system, "permanent": matched_permanent} def _get_user_listing(self, user_id: int) -> Optional[PlayerListing]: cursor = get_db().conn.cursor() @@ -799,9 +854,17 @@ class WPSStoreSystem(WPSAPI): row = cursor.fetchone() if not row: return None + item_id = row["item_id"] + backpack: WPSBackpackSystem = Architecture.Get(WPSBackpackSystem) + try: + definition = backpack._get_definition(item_id) # type: ignore[attr-defined] + item_name = definition.name + except Exception: + item_name = item_id return PlayerListing( user_id=row["user_id"], - item_id=row["item_id"], + item_id=item_id, + item_name=item_name, price=row["price"], quantity=row["quantity"], created_at=self._parse_datetime(row["created_at"]), @@ -975,25 +1038,38 @@ class WPSStoreSellCommand(WPSAPI): message = self.parse_message_after_at(message).strip() if not message: return await self._send_error( - "❌ 出售指令格式错误,请使用:`出售 <物品名称或ID> <数量>`", + "❌ 出售指令格式错误,请使用:`出售 <物品名称或ID> <数量> <单价>`", chat_id, user_id, ) tokens = [token.strip() for token in message.split() if token.strip()] - if len(tokens) < 2: + if len(tokens) < 3: return await self._send_error( - "❌ 出售指令格式错误,请使用:`出售 <物品名称或ID> <数量>`", + "❌ 出售指令格式错误,请使用:`出售 <物品名称或ID> <数量> <单价>`", chat_id, user_id, ) - identifier = " ".join(tokens[:-1]).strip() - quantity_token = tokens[-1] + identifier = " ".join(tokens[:-2]).strip() + quantity_token = tokens[-2] + price_token = tokens[-1] + if not identifier: + return await self._send_error( + "❌ 出售指令格式错误,请提供物品名称或ID", + chat_id, + user_id, + ) try: quantity = int(quantity_token) except ValueError: return await self._send_error("❌ 出售数量必须是整数", chat_id, user_id) + try: + price = int(price_token) + except ValueError: + return await self._send_error("❌ 出售单价必须是整数", chat_id, user_id) + if price < 0: + return await self._send_error("❌ 出售单价必须是非负整数", chat_id, user_id) store_api = Architecture.Get(WPSStoreSystem) response = await store_api.sell_item( @@ -1001,6 +1077,7 @@ class WPSStoreSellCommand(WPSAPI): user_id=user_id, identifier=identifier, quantity=quantity, + price=price, ) return await self.send_markdown_message(response, chat_id, user_id)