Ver código fonte

feat(经济系统): 新增经济系统插件及相关功能

添加经济系统插件,包括金币的增加、减少、查询功能,并支持HTML模板渲染。修改了.gitignore、配置文件、接口定义、装饰器逻辑等,以支持新功能。同时,修复了部分路径和权限问题,优化了日志记录和日期格式化。
枫林 3 meses atrás
pai
commit
aba937b5b8

+ 2 - 1
.gitignore

@@ -81,5 +81,6 @@ src/plugins/*
 !src/plugins/PluginsFile.ts
 !src/plugins/test.ts
 !src/plugins/log.ts
+!src/plugins/ecomony.ts
 
-src/data
+/data

+ 0 - 12
data/economy/2180323481.json

@@ -1,12 +0,0 @@
-{
-    "userId": "2180323481",
-    "coins": 10000,
-    "logs": [
-        {
-            "type": "add",
-            "amount": 10000,
-            "reason": "添加金币",
-            "date": "2025-05-23T23:50:39.034Z"
-        }
-    ]
-}

+ 1 - 1
src/config/economy.yml

@@ -4,5 +4,5 @@ enable: true # 是否启用经济系统
 currency: 元 # 货币单位
 decimal: 2 # 小数位数
 data:
-  path: "/Users/fenglin/Desktop/botQQ/src/data" # 数据路径
+  path: "/Users/fenglin/Desktop/botQQ/data" # 数据路径
   defaultCoins: 0 # 默认金额

+ 8 - 0
src/config/permission.yml

@@ -59,6 +59,14 @@ users:
       sakulass:
         commands:
           图: true
+      ecomony:
+        commands:
+          info: true
+          add: true
+          help: true
+      test:
+        commands:
+          remove: true
   default:
     plugins:
       test:

+ 1 - 1
src/interface/economy.ts

@@ -7,7 +7,7 @@ export interface Economylogs {
     type: 'add' | 'remove';
     amount: number;
     reason: string;
-    date: Date;
+    date: string;
 }
 export interface EconomyCommands {
     name: string; 

+ 2 - 2
src/lib/decorators.ts

@@ -279,14 +279,14 @@ export function schedule(cron: string): MethodDecorator {
  * @param type - 操作类型: 'add' 或 'remove'
  * @param reason - 操作原因
  */
-export function coins(amount: number, type: 'add' | 'remove' ,reason: string = "未知原因") {
+export function coins(amount: number, type: 'add' | 'remove' ,reason?: string) {
     return function (target: any, propertyKey: string | symbol | undefined): void {
         const actualPropertyKey = propertyKey!;
         const fnName = `${target.constructor.name}.${actualPropertyKey.toString()}`;
         const EconomyCommand: EconomyCommands = {
             amount: amount,
             type: type,
-            reason: reason,
+            reason: reason??`执行指令: ${CMD_PREFIX}${fnName} ${type=='add'? '增加':'减少'} ${amount}`,
             name: ''
         };
         economyCommands.set(fnName,{...EconomyCommand})

+ 22 - 5
src/lib/economy.ts

@@ -7,11 +7,11 @@ import { economy } from "./config.js";
 export function addCoins(userId: string, amount: number, reason: string): void {
     const userData = getUserData(userId);
     userData.coins += amount;
-    userData.logs.push({
+    userData.logs.unshift({
         type: 'add',
         amount: amount,
         reason: reason,
-        date: new Date()
+        date: formatDate(new Date())
     });
     saveUserData(userId, userData);
 }
@@ -21,18 +21,25 @@ export function removeCoins(userId: string, amount: number, reason: string): voi
         throw new Error(`${economy.name}不足,需要${amount}${economy.currency},拥有${userData.coins}${economy.currency}`);
     }
     userData.coins -= amount;
-    userData.logs.push({
+    userData.logs.unshift({
         type: 'remove',
         amount: amount,
         reason: reason,
-        date: new Date()
+        date: formatDate(new Date())
     });
      saveUserData(userId, userData);
 }
-function getUserData(userId: string): UserData {
+export function getUserData(userId: string): UserData {
     if (!fs.existsSync(`${economy.data.path}`)) {
         throw new Error(`未找到用户数据目录,请检查配置文件`);
     }
+    if (!userId) {
+        return {
+            userId: '',
+            coins: 0,
+            logs: []
+        }
+    }
     if (!fs.existsSync(`${economy.data.path}/${userId}.json`)) {
         const newUserData: UserData = {
             userId: userId,
@@ -50,4 +57,14 @@ function  saveUserData(userId: string, userData: UserData): void {
         throw new Error(`未找到用户数据目录,请检查配置文件`);
     }
     fs.writeFileSync(`${economy.data.path}/${userId}.json`, JSON.stringify(userData, null, 4));
+}
+//格式化时间
+export function formatDate(date: Date): string {
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    const seconds = String(date.getSeconds()).padStart(2, '0');
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
 }

+ 211 - 0
src/plugins/ecomony.ts

@@ -0,0 +1,211 @@
+import { addCoins, getUserData, removeCoins } from "../lib/economy.js";
+import { param, plugins, runcod } from "../lib/decorators.js";
+import { GroupMessage, PrivateFriendMessage, PrivateGroupMessage } from "node-napcat-ts/dist/Interfaces.js";
+import path from "path";
+import { fileURLToPath } from "url";
+import { ParamType } from "../interface/plugin.js";
+import { IsAdmin } from "../lib/Permission.js";
+
+@plugins({
+    name: "经济系统", //插件名称,用于显示在菜单中
+    version: "1.0.0", //插件版本号,用于显示在菜单中
+    describe: "官方经济插件", //插件描述,用于显示在菜单中
+    author: "枫叶秋林",//插件作者,用于显示在菜单中
+    help: { //插件帮助信息,用于显示在菜单中
+        enabled: true, //是否启用帮助信息
+        description: "显示帮助信息" //帮助信息描述
+    }
+})
+export class ecomony {
+    @runcod(["info","个人信息"], "获取个人金币信息")
+    async ecomonyInfo(
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        const { userId,coins,logs } = await getUserData(context?.sender?.user_id?.toString())
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        return {
+            nickname: context?.sender?.nickname??"未知",
+            coins: coins,
+            logs: logs,
+            avatar: `http://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`,
+            template: {
+                enabled: true,
+                sendText: false,
+                path: path.resolve(__dirname, '..', 'resources', 'ecomony', 'info.html'),//模版路径,推荐按规范放置在resources目录下
+                render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    width: 800, // 模板宽度
+                    height: 600,// 模板高度
+                    type: 'png',// 模板类型
+                    quality: 100,// 模板质量
+                    fullPage: false,// 是否全屏
+                    background: true// 是否背景
+                }
+            },
+            toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                let logsString = "";
+                logs.forEach(log => {
+                    logsString += `类型: ${log.type} 数量: ${log.amount} 原因: ${log.reason} 时间: ${log.date}\n`;
+                });
+                return `
+                    金币: ${coins}\n
+                    ------明细记录----
+                    ${logsString}
+                `;
+            }   
+        }
+    }
+
+    @runcod(["add", "增加"], "增加金币")
+    async addecomony(
+        @param("QQ号", ParamType.Number,) userid: string,
+        @param("数量", ParamType.Number) amount: number,
+        @param("原因", ParamType.String,'管理员增加',true) reason: string,
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) {
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        try {
+            if (!IsAdmin(context.sender.user_id)) {
+                return {
+                    msgtype: 'error',
+                    ecomsg: `无权限,无法增加金币`,
+                    template: {
+                        enabled: true,
+                        sendText: false,
+                        path: path.resolve(__dirname, '..','resources', 'ecomony','msg.html'),//模版路径,推荐按规范放置在resources目录下
+                        render: {//浏览器默认参数设置,用于打开浏览器的设置
+                            width: 800, // 模板宽度
+                            height: 600,// 模板高度
+                            type: 'png',// 模板类型
+                            quality: 100,// 模板质量
+                            fullPage: false,// 是否全屏
+                            background: true// 是否背景
+                        }
+                    }
+                }
+            }
+            addCoins(context.sender.user_id.toString(),amount,reason)
+            const newcoins = (await getUserData(userid)).coins
+            return {
+                msgtype: 'success',
+                ecomsg: `增加成功! 金币 +${amount}, 当前数量: ${newcoins}`,
+                template: {
+                    enabled: true,
+                    sendText: false,
+                    path: path.resolve(__dirname, '..', 'resources', 'ecomony', 'msg.html'),//模版路径,推荐按规范放置在resources目录下
+                    render: {//浏览器默认参数设置,用于打开浏览器的设置
+                        width: 800, // 模板宽度
+                        height: 400,// 模板高度
+                        type: 'png',// 模板类型
+                        quality: 100,// 模板质量
+                        fullPage: false,// 是否全屏
+                        background: true// 是否背景
+                    }
+                },
+                toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                    return `
+                        增加成功\n
+                        数量: ${amount}\n
+                        原因: ${reason}\n
+                        时间: ${new Date().toLocaleString()}\n
+                    `;
+                }   
+            }
+        } catch (error) {
+            return {
+                type: 'error',
+                ecomsg: `增加失败! 原因: ${(error as Error).message??'未知错误'}`,
+                template: {
+                    enabled: true,
+                    sendText: false,
+                    path: path.resolve(__dirname, '..', 'resources', 'ecomony', 'msg.html'),//模版路径,推荐按规范放置在resources目录下
+                    render: {//浏览器默认参数设置,用于打开浏览器的设置
+                        width: 800, // 模板宽度
+                        height: 400,// 模板高度
+                        type: 'png',// 模板类型
+                        quality: 100,// 模板质量
+                        fullPage: false,// 是否全屏
+                        background: true// 是否背景
+                    }
+                },
+                toString() { //重写toString方法,用于返回文本内容,启用sendText时将发送文本内容,不启用时将发送图片内容,图片发送失败时发送文字内容
+                    return `
+                        增加成功\n
+                        数量: ${amount}\n
+                        原因: ${reason}\n
+                        时间: ${new Date().toLocaleString()}\n
+                    `;
+                }   
+            }
+        }
+        
+    }
+
+    @runcod(["reduce", "减少"], "减少金币")
+    async reduceecomony(
+        @param("QQ号", ParamType.Number,) userid: string,
+        @param("数量", ParamType.Number) amount: number,
+        @param("原因", ParamType.String,'管理员减少',true) reason: string,
+        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ){
+        const __dirname = path.dirname(fileURLToPath(import.meta.url)); //获取当前文件的目录名
+        try {
+            if (!IsAdmin(context.sender.user_id)) {
+                return {
+                    msgtype: 'error',
+                    ecomsg: `无权限,无法减少金币`,
+                    template: {
+                        enabled: true,
+                        sendText: false,
+                        path: path.resolve(__dirname, '..','resources', 'ecomony','msg.html'),//模版路径,推荐按规范放置在resources目录下
+                        render: {//浏览器默认参数设置,用于打开浏览器的设置
+                            width: 800, // 模板宽度
+                            height: 400,// 模板高度
+                            type: 'png',// 模板类型
+                            quality: 100,// 模板质量
+                            fullPage: false,// 是否全屏
+                            background: true// 是否背景
+                        }
+                    }
+                }
+            }
+            removeCoins(context.sender.user_id.toString(),-amount,reason)
+            const newcoins = (await getUserData(userid)).coins
+            return {
+                msgtype:'success',
+                ecomsg: `减少成功! 金币 -${amount}, 当前数量: ${newcoins}`,
+                template: {
+                    enabled: true,
+                    sendText: false,
+                    path: path.resolve(__dirname, '..','resources', 'ecomony','msg.html'),//模版路径,推荐按规范放置在resources目录下
+                    render: {//浏览器默认参数设置,用于打开浏览器的设置
+                        width: 800, // 模板宽度
+                        height: 400,// 模板高度
+                        type: 'png',// 模板类型
+                        quality: 100,// 模板质量
+                        fullPage: false,// 是否全屏
+                        background: true// 是否背景
+                    }
+                }
+            }
+        } 
+        catch (error) {
+            return {
+                msgtype: 'error',
+                ecomsg: `减少失败! 原因: ${(error as Error).message??'未知错误'}`,
+                template: {
+                    enabled: true,
+                    sendText: false,
+                    path: path.resolve(__dirname, '..','resources', 'ecomony','msg.html'),//模版路径,推荐按规范放置在resources目录下
+                    render: {//浏览器默认参数设置,用于打开浏览器的设置
+                        width: 800, // 模板宽度
+                        height: 400,// 模板高度
+                        type: 'png',// 模板类型
+                        quality: 100,// 模板质量
+                        fullPage: false,// 是否全屏
+                        background: true// 是否背景
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 11
src/plugins/test.ts

@@ -71,20 +71,10 @@ export class test {
         };
     }
 
-    @runcod(["add"], "添加金币")//命令装饰器,用于注册命令
-    @coins(10000,//金币数量
-        'add',//类别 add为增加金币,remove为减少金币
-        "添加金币"//原因,用于记录日志
-    )
-    async add(){
-        return `添加成功`;
-    }
-    //remove coins from user
     @runcod(["remove"], "移除金币")//命令装饰器,用于注册命令
     @coins(
-        10000,//金币数量
+        10,//金币数量
         'remove',//类别 add为增加金币,remove为减少金币
-        "移除金币"//原因,用于记录日志
     ) //经济修饰词,用于减少金币
     async remove(){
         return `移除成功`;

+ 118 - 0
src/resources/ecomony/info.html

@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+
+    <style>
+        .card {
+            background: white;
+            border-radius: 12px;
+            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+            padding: 20px;
+            margin: 15px;
+        }
+        .user-avatar {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            border-radius: 50%;
+            border: 2px solid #fff;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+            transition: transform 0.3s ease;
+        }
+        .user-profile {
+            display: flex;
+            align-items: center;
+            gap: 20px;
+            padding-bottom: 15px;
+            border-bottom: 1px solid #eee;
+        }
+
+        .avatar-container {
+            width: 80px;
+            height: 80px;
+            border-radius: 50%;
+            overflow: hidden;
+            flex-shrink: 0;
+        }
+
+        .user-info {
+            display: flex;
+            flex-direction: column;
+            gap: 5px;
+        }
+
+        .nickname {
+            font-size: 1.2em;
+            font-weight: 500;
+        }
+
+        .coins {
+            color: #666;
+            font-size: 0.9em;
+        }
+
+        .transaction-list {
+            list-style: none;
+            padding: 0;
+            margin: 15px 0 0;
+        }
+
+        .transaction-item {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            padding: 12px 0;
+            border-bottom: 1px solid #f5f5f5;
+        }
+
+        .transaction-details {
+            display: flex;
+            flex-direction: column;
+            gap: 3px;
+        }
+
+        .transaction-amount {
+            font-weight: 500;
+        }
+
+        @media (max-width: 600px) {
+            .card {
+                margin: 10px;
+                padding: 15px;
+            }
+            
+            .user-profile {
+                gap: 15px;
+            }
+        }
+    </style>
+</head>
+<body>
+    <div class="card">
+        <!-- 用户信息区块 -->
+        <div class="user-profile">
+            <div class="avatar-container">
+                <img src="{{avatar}}" class="user-avatar">
+            </div>
+            <div class="user-info">
+                <span class="nickname">{{nickname}}</span>
+                <span class="coins">🪙 当前金币:{{coins}}</span>
+            </div>
+        </div>
+
+        <!-- 交易记录列表 -->
+        <ul class="transaction-list">
+            {{each logs}}
+            <li class="transaction-item">
+                <div class="transaction-details">
+                    <div class="transaction-time">{{$value.date}}</div>
+                    <div class="transaction-reason">{{$value.reason}}</div>
+                </div>
+                <div class="transaction-amount">
+                    {{if $value.type=='add'}}<span style="color: rgb(50, 110, 50)" >+ {{$value.amount}}</span>{{else}}<span style="color: rgb(230, 56, 56);">-{{$value.amount}}</span>{{/if}}
+                </div>
+            </li>
+            {{/each}}
+        </div>
+    </div>
+</body>
+</html>

+ 63 - 0
src/resources/ecomony/msg.html

@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        .msg-card {
+            border-radius: 8px;
+            padding: 16px;
+            margin: 12px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            display: flex;
+            gap: 12px;
+            border-left: 4px solid;
+        }
+
+        .msg-card.success {
+            border-color: #4CAF50;
+            background: #f1f8e9;
+        }
+
+        .msg-card.error {
+            border-color: #f44336;
+            background: #ffebee;
+        }
+
+        .icon-wrapper {
+            font-size: 1.5em;
+            flex-shrink: 0;
+        }
+
+        .success .icon-wrapper { color: #2e7d32; }
+        .error .icon-wrapper { color: #c62828; }
+
+        .msg-content {
+            flex-grow: 1;
+        }
+
+        .msg-title {
+            font-weight: 500;
+            margin-bottom: 4px;
+        }
+
+        @media (max-width: 480px) {
+            .msg-card {
+                margin: 8px;
+                padding: 12px;
+            }
+        }
+    </style>
+</head>
+<body>
+    <div class="msg-card {{msgtype}}">
+        <div class="icon-wrapper">
+            {{if msgtype == 'success'}}✅{{else}}❌{{/if}}
+        </div>
+        <div class="msg-content">
+            <div class="msg-title">
+                {{if msgtype == 'success'}}操作成功{{else}}发生错误{{/if}}
+            </div>
+            <div class="msg-text">{{ecomsg}}</div>
+        </div>
+    </div>
+</body>
+</html>