diff --git a/api_config.json b/api_config.json index 0521928..9d7241e 100644 --- a/api_config.json +++ b/api_config.json @@ -47,6 +47,44 @@ "adventureName": "阴阳两界", "serverName": "眉间雪" } + }, + "aijx3_SearchData":{ + "url":"https://www.aijx3.cn/api2/aijx3-wblwg/basedata/getSearchData", + "method":"POST", + "description":"获取爱剑网三所有外观数据", + "params":{ + } + }, + "jx3box_exterior":{ + "url":"https://node.jx3box.com/api/node/v2/exterior/list", + "method":"GET", + "description":"魔盒中模糊查找剑网三的外观数据", + "params":{ + "keyword": "秃盒", + "tradable": "0", + "list_type": "0" + } + }, + "aijx3_GoodsDetail":{ + "url":"https://www.aijx3.cn/api2/aijx3-wj/goods/getGoodsDetail", + "method":"POST", + "description":"在爱剑网三中获取外观详细数据", + "params":{ + "goodsName": "秃盒" + } + }, + "aijx3_wblwg":{ + "url":"https://www.aijx3.cn/api2/aijx3-wblwg/record/queryByCondition", + "method":"POST", + "description":"在爱剑网三中获取外观详细数据", + "params":{ + "accoSeq": "", + "orderMode": 1, + "orderBy": "price_num", + "searchId": [48356], + "current": 1, + "size": 10, + "tradeStatus": "" + } } - } \ No newline at end of file diff --git a/core/basic_Function.py b/core/basic_Function.py new file mode 100644 index 0000000..b4719ac --- /dev/null +++ b/core/basic_Function.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path + +def load_template(template_name): + """ + 从模板文件加载模板内容 + + Args: + template_name: 模板文件名(不带路径) + + Returns: + str: 模板内容 + """ + # 获取模板文件路径 + plugin_dir = Path(__file__).parent.parent + template_path = plugin_dir / "templates" / template_name + + # 检查文件是否存在 + if not template_path.exists(): + raise FileNotFoundError(f"模板文件不存在: {template_path}") + + # 读取模板内容 + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + + +def extract_field(data_list, field_name): + """ + 从列表中的字典提取指定字段的所有值 + + Args: + data_list (list): 数据列表,每个元素是 dict + field_name (str): 要提取的字段名 + + Returns: + list: 提取出来的字段值列表 + """ + extracted_data = [] + for item in data_list: + if field_name in item and item[field_name]: + # 如果是 list,展开 + if isinstance(item[field_name], list): + extracted_data.extend(item[field_name]) + else: + extracted_data.append(item[field_name]) + return extracted_data \ No newline at end of file diff --git a/core/jx3_Function.py b/core/jx3_Function.py index 91726d4..57c73fd 100644 --- a/core/jx3_Function.py +++ b/core/jx3_Function.py @@ -3,11 +3,13 @@ from datetime import datetime from astrbot.api import logger from .api_data import APIClient -from .load_template import load_template +from .sql_data import AsyncMySQL +from .basic_Function import load_template, extract_field class JX3Function: - def __init__(self, api_config): + def __init__(self, api_config,db: AsyncMySQL ): self.__api = APIClient() + self.__db = db self.__api_config = api_config async def richang(self,server: str = "眉间雪",num: int = 0): @@ -199,4 +201,184 @@ class JX3Function: return_data["msg"] = "系统错误:模板渲染数据准备失败" return return_data return_data["code"] = 200 - return return_data \ No newline at end of file + return return_data + + + async def SearchData(self): + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_SearchData"] + # 获取数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取接口信息失败" + return return_data + # 提取数据 + try: + extracted_data = extract_field(data, "dataModels") + except FileNotFoundError as e: + logger.error(f"提取数据失败: {e}") + return_data["msg"] = "提取指定数据字段失败" + return return_data + # 准备SQL和批量数据 + sql = """ + INSERT INTO searchdata + (typeName, name, showName, picUrl, searchId, searchDescType) + VALUES (%s, %s, %s, %s, %s, %s) + """ + values_list = [ + ( + item['typeName'], + item['name'], + item['showName'], + item['picUrl'], + item['searchId'], + item['searchDescType'] + ) + for item in extracted_data + ] + # 插入数据 + try: + await self.__db.truncate_table("searchdata") + await self.__db.executemany(sql, values_list) + except FileNotFoundError as e: + logger.error(f"数据插入失败: {e}") + return_data["msg"] = "数据插入数据库失败" + return return_data + return_data["msg"] = f"成功批量插入 {len(extracted_data)} 条数据!" + return_data["code"] = 200 + return return_data + + + async def wujia(self,Name: str = "秃盒"): + return_data = { + "code": 0, + "msg": "功能函数未执行", + "data": {} + } + #在配置文件中获取接口配置 + api_config = self.__api_config["jx3box_exterior"] + #更新参数 + api_config["params"]["keyword"] = Name + # 获取查找信息 + data = await self.__api.get(api_config["url"],api_config["params"],"data") + if data["total"] == 0: + try: + sql = "SELECT showName, searchId FROM searchdata WHERE name = %s OR showName = %s" + sqldata = await self.__db.fetch_one(sql, (Name,Name)) + searchId=sqldata["searchId"] + showName=sqldata["showName"] + return_data["data"]["searchId"]=searchId + return_data["data"]["showName"]=showName + except Exception as e: + logger.error(f"获取外观信息错误: {e}") + return_data["msg"] = "未找到该外观信息" + return return_data + else: + # 查询魔盒获取外观名称 + try: + showName=data["list"][0]["name"] + return_data["data"]["showName"]=showName + except Exception as e: + logger.error(f"获取外观名称错误: {e}") + return_data["msg"] = "未找到该外观信息" + return return_data + # 查询爱剑三获取外观ID + try: + sql = "SELECT searchId FROM searchdata WHERE showName=%s" + sqldata = await self.__db.fetch_one(sql, (return_data["data"]["showName"],)) + searchId=sqldata["searchId"] + return_data["data"]["searchId"]=searchId + except Exception as e: + logger.error(f"获取外观ID错误: {e}\n{return_data['data']['showName']}") + return_data["msg"] = "未找到该外观信息" + return return_data + + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_GoodsDetail"] + #更新参数 + api_config["params"]["goodsName"] = showName + # 获取数据外观详细数据 + data = await self.__api.post(api_config["url"],api_config["params"],"data") + if not data: + return_data["msg"] = "获取外观详细数据失败" + return return_data + # 提取外观详细数据 + try: + imgs = data.get("imgs", []) + return_data["data"]["goodsDesc"]=data.get("goodsDesc", "无描述") + return_data["data"]["publishTime"]=data.get("publishTime", "无价格") + return_data["data"]["priceNum"]=data.get("priceNum", 0) + return_data["data"]["goodsId"]=data.get("goodsId", "无数据") + return_data["data"]["imgs"]=imgs[0] if imgs else "" + return_data["data"]["goodsAlias"]=data.get("goodsAlias", "无别名") + except Exception as e: + logger.error(f"提取外观详细数据失败: {e}") + return_data["msg"] = "提取外观详细数据失败" + return return_data + + # 查询万宝楼数据(公示和在售) + wbl_data = await self.__get_wbl_data(searchId) + if not wbl_data: + return_data["msg"] = "获取万宝楼数据失败" + return return_data + if wbl_data: + return_data["data"]["wblgs"]=wbl_data["wblgs"] + return_data["data"]["wblzs"]=wbl_data["wblzs"] + return_data["msg"] = "获取万宝楼数据完成" + # 加载模板 + try: + return_data["temp"] = load_template("wujia.html") + except FileNotFoundError as e: + logger.error(f"加载模板失败: {e}") + return_data["msg"] = "系统错误:模板文件不存在" + return return_data + return_data["code"] = 200 + return return_data + + async def __get_wbl_data(self,search_id): + """获取万宝楼数据(公示和在售)""" + try: + #在配置文件中获取接口配置 + api_config = self.__api_config["aijx3_wblwg"] + #更新参数 + api_config["params"]["searchId"] = [search_id] + # 获取公示数据 + api_config["params"]["tradeStatus"] = "3" + datawblgs = await self.__api.post(api_config["url"], api_config["params"], "data") + # 获取在售数据 + api_config["params"]["tradeStatus"] = "5" + datawblzs = await self.__api.post(api_config["url"], api_config["params"], "data") + + return { + "wblgs": await self.__process_wbl_records(datawblgs.get("records", [])), + "wblzs": await self.__process_wbl_records(datawblzs.get("records", [])) + } + except Exception as e: + logger.error(f"获取万宝楼数据出错: {e}") + return None + + async def __process_wbl_records(self,records): + """处理万宝楼记录数据""" + processed = [] + for record in records: + try: + # 转换时间戳为可读格式 + timestamp = record.get("replyTime", 0) + dt = datetime.fromtimestamp(timestamp / 1000) if timestamp else datetime.now() + + processed.append({ + "priceNum": record.get("priceNum", 0), + "belongQf2": record.get("belongQf2", "无数据"), + "replyTime": dt.strftime("%Y-%m-%d %H:%M:%S"), + "discountRate": record.get("discountRate", 0.0), + }) + except Exception as e: + logger.error(f"处理万宝楼记录出错: {e}") + continue + + return processed \ No newline at end of file diff --git a/core/jx3_data.py b/core/jx3_data.py index cea35c5..b6dda24 100644 --- a/core/jx3_data.py +++ b/core/jx3_data.py @@ -4,7 +4,6 @@ from urllib.parse import quote from datetime import datetime from .api_data import api_data_get, api_data_post -from .sql_data import sql_data_searchdata,sql_data_select def fetch_all_pages(base_url, initial_params, max_pages=None): @@ -204,7 +203,7 @@ def jx3_data_wujia(inname="秃盒"): try: # 1. 获取物品ID和名称 - idname = sql_data_select(inname) + idname = api_data_get(inname) if not idname: datas.update({"code": 201, "msg": "未找到该外观信息"}) return datas diff --git a/core/load_template.py b/core/load_template.py deleted file mode 100644 index c8f3363..0000000 --- a/core/load_template.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -from pathlib import Path - -def load_template(template_name): - """ - 从模板文件加载模板内容 - - Args: - template_name: 模板文件名(不带路径) - - Returns: - str: 模板内容 - """ - # 获取模板文件路径 - plugin_dir = Path(__file__).parent.parent - template_path = plugin_dir / "templates" / template_name - - # 检查文件是否存在 - if not template_path.exists(): - raise FileNotFoundError(f"模板文件不存在: {template_path}") - - # 读取模板内容 - with open(template_path, 'r', encoding='utf-8') as f: - return f.read() \ No newline at end of file diff --git a/core/sql_data.py b/core/sql_data.py index 96febca..9adeb17 100644 --- a/core/sql_data.py +++ b/core/sql_data.py @@ -1,107 +1,63 @@ -import pymysql -import time -from astrbot.api import logger -from .api_data import api_data_get, api_data_post +import aiomysql -#连接数据库配置 -db_config = { - 'host': '154.201.70.116', - 'port': 3306, - 'user': 'asrtbot', - 'password': 'qsc123456', - 'database': 'asrtbot', - 'charset': 'utf8mb4', - 'cursorclass': pymysql.cursors.DictCursor #返回字典格式数据 -} +class AsyncMySQL: + def __init__(self, db_config: dict): + self.db_config = db_config + self.pool = None -# 提取所有 dataModels 中的数据 -def extract_data_models(source_data): - extracted_data = [] - for category in source_data: - if "dataModels" in category and category["dataModels"]: - extracted_data.extend(category["dataModels"]) - return extracted_data + async def init_pool(self): + """初始化连接池""" + if self.pool is None: + self.pool = await aiomysql.create_pool(**self.db_config) -# 获取并存储搜索数据 -def sql_data_searchdata(): - # 接口配置 - custom_url = "https://www.aijx3.cn/api2/aijx3-wblwg/basedata/getSearchData" - params = {} - - try: - # 获取数据 - source_data = api_data_post(custom_url, params, "data") - if not source_data: - return "获取数据失败或数据为空" - - # 处理数据 - extracted_data = extract_data_models(source_data) - if not extracted_data: # 修正:应该是extracted_data而不是source_data - return "未提取到数据" - - # 连接数据库并插入数据 - with pymysql.connect(**db_config) as connection: - with connection.cursor() as cursor: - # 清空表数据 - cursor.execute("TRUNCATE TABLE searchdata") - - # 准备SQL和批量数据 - sql = """ - INSERT INTO searchdata - (typeName, name, showName, picUrl, searchId, searchDescType) - VALUES (%s, %s, %s, %s, %s, %s) - """ - - # 使用列表推导式简化数据准备 - values_list = [ - ( - item['typeName'], - item['name'], - item['showName'], - item['picUrl'], - item['searchId'], - item['searchDescType'] - ) - for item in extracted_data - ] - - # 批量插入 - cursor.executemany(sql, values_list) - connection.commit() - - return f"成功批量插入 {len(extracted_data)} 条数据!" - - except pymysql.Error as e: - return f"数据库操作失败: {e}" - except Exception as e: - return f"操作失败: {e}" + async def close_pool(self): + """关闭连接池""" + if self.pool: + self.pool.close() + await self.pool.wait_closed() + self.pool = None -# 根据搜索字符串查询匹配的数据 -def sql_data_select(search_string): - """ - 根据搜索字符串查询匹配的数据 - - Args: - search_string: 搜索字符串 - - Returns: - 匹配的第一条记录,如果没有匹配则返回None - """ - try: - with pymysql.connect(**db_config) as connection: - with connection.cursor() as cursor: - sql = """ - SELECT searchId, showName, name - FROM searchdata - WHERE name = %s OR showName = %s - LIMIT 1 - """ - params = (search_string, search_string) + async def fetch_one(self, sql: str, params=None): + """查询单条数据""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(sql, params or ()) + return await cursor.fetchone() - cursor.execute(sql, params) - return cursor.fetchone() - - except Exception as e: - logger.error(f"查询数据时出错: {e}") - return None \ No newline at end of file + async def fetch_all(self, sql: str, params=None): + """查询多条数据""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute(sql, params or ()) + return await cursor.fetchall() + + async def execute(self, sql: str, params=None): + """执行单条 SQL(insert/update/delete)""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.execute(sql, params or ()) + await conn.commit() + return cursor.rowcount + + async def executemany(self, sql: str, params_list): + """批量执行 SQL""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.executemany(sql, params_list) + await conn.commit() + return cursor.rowcount + + async def truncate_table(self, table_name: str): + """清空指定表的内容""" + await self.init_pool() + async with self.pool.acquire() as conn: + async with conn.cursor() as cursor: + sql = f"TRUNCATE TABLE `{table_name}`" + await cursor.execute(sql) + await conn.commit() + return True diff --git a/main.py b/main.py index 7296bf9..4119a9e 100644 --- a/main.py +++ b/main.py @@ -7,9 +7,9 @@ from astrbot.api.star import Context, Star, register, StarTools from astrbot.api import logger from astrbot.api import AstrBotConfig -from .core.load_template import load_template +from .core.basic_Function import load_template from .core.jx3_data import jx3_data_jiaoyihang,jx3_data_wujia -from .core.sql_data import sql_data_searchdata,sql_data_select +from .core.sql_data import AsyncMySQL from .core.jx3_Function import JX3Function @@ -31,12 +31,25 @@ class Jx3ApiPlugin(Star): # 读取文件内容 with open(self.api_file_path, 'r', encoding='utf-8') as f: self.api_config = json.load(f) + logger.info("jx3api插件初始化完成") + async def initialize(self): """可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。""" + # 数据库配置 + db_config = { + 'host': '154.201.70.116', + 'port': 3306, + 'user': 'asrtbot', + 'password': 'qsc123456', + 'db': 'asrtbot', + 'charset': 'utf8mb4', + 'autocommit': True + } #创建类实例 - self.jx3fun = JX3Function(self.api_config) + self.db = AsyncMySQL(db_config) + self.jx3fun = JX3Function(self.api_config,self.db) logger.info("jx3api插件创建实例完成") @@ -136,6 +149,38 @@ class Jx3ApiPlugin(Star): yield event.plain_result("猪脑过载,请稍后再试") + @jx3.command("物价") + async def jx3_wujia(self, event: AstrMessageEvent,Name: str = "秃盒"): + """剑三 外观名称""" + try: + data=await self.jx3fun.wujia(Name) + if data["code"] == 200: + url = await self.html_render(data["temp"], data["data"], options={}) + yield event.image_result(url) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + + @filter.permission_type(filter.PermissionType.ADMIN) + @jx3.command("外观数据同步") + async def jx3_SearchData(self, event: AstrMessageEvent): + """剑三外观数据同步""" + try: + data=await self.jx3fun.SearchData() + if data["code"] == 200: + yield event.plain_result(data["msg"]) + else: + yield event.plain_result(data["msg"]) + return + except Exception as e: + logger.error(f"功能函数执行错误: {e}") + yield event.plain_result("猪脑过载,请稍后再试") + + @filter.command("剑三交易行") async def jx3_data_jiaoyihang(self, event: AstrMessageEvent): """剑三交易行 物品名称 服务器""" @@ -192,21 +237,11 @@ class Jx3ApiPlugin(Star): yield event.plain_result("查询交易行数据时出错,请稍后再试") - @filter.command("剑三外观数据同步") - async def jx3_SearchData(self, event: AstrMessageEvent): - """剑三外观数据同步""" - - try: - test = sql_data_searchdata() - yield event.plain_result(f"{test}") - - except Exception as e: - logger.error(f"处理数据时出错: {e}") - yield event.plain_result("处理接口返回信息时出错") + @filter.command("剑三物价") - async def jx3_wujia(self, event: AstrMessageEvent): + async def jx3_wujia11(self, event: AstrMessageEvent): """剑三物价 外观名称""" inname = "秃盒" @@ -244,4 +279,6 @@ class Jx3ApiPlugin(Star): async def terminate(self): - """可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。""" \ No newline at end of file + """可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。""" + await self.db.close_pool() + logger.info("jx3api插件已卸载/停用") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9e7dd9d..63c276a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pymysql \ No newline at end of file +aiomysql diff --git a/templates/wujia.html b/templates/wujia.html index a04adbc..620ce71 100644 --- a/templates/wujia.html +++ b/templates/wujia.html @@ -3,7 +3,7 @@
-商品ID: {{ data.goodsId }} | 搜索ID: {{ data.searchId }}
+ +{{ data.goodsDesc }}
| 上架时间 | +服务器 | +价格 | +折扣率 | +
|---|---|---|---|
| {{ item.replyTime }} | +{{ item.belongQf2 }} | +{{ item.priceNum }}元 | ++ + {{ item.discountRate }}% + + | +
| 价格 | -服务器 | -折扣率 | -更新时间 | -操作 | -
|---|---|---|---|---|
| {{ item.priceNum }}元 | -{{ item.belongQf2 }} | -- - {{ item.discountRate }}% - - | -{{ item.replyTime }} | -- |
| 价格 | -服务器 | -折扣率 | -更新时间 | -
|---|---|---|---|
| {{ item.priceNum }}元 | -{{ item.belongQf2 }} | -- - {{ item.discountRate }}% - - | -{{ item.replyTime }} | -
| 上架时间 | +服务器 | +价格 | +折扣率 | +
|---|---|---|---|
| {{ item.replyTime }} | +{{ item.belongQf2 }} | +{{ item.priceNum }}元 | ++ + {{ item.discountRate }}% + + | +
剑网3万宝楼商品数据展示 © 2025 | 数据仅供参考,实际价格以游戏内为准