koishi-plugin-class-score-system 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/menu.d.ts +2 -0
- package/lib/index.js +1922 -0
- package/lib/utils/constants.d.ts +48 -0
- package/lib/utils/image.d.ts +10 -0
- package/package.json +27 -0
- package/src/commands/adjust-score.ts +68 -0
- package/src/commands/admin-manager.ts +340 -0
- package/src/commands/bind-qq.ts +112 -0
- package/src/commands/bind-server.ts +92 -0
- package/src/commands/menu.ts +243 -0
- package/src/commands/query-score.ts +136 -0
- package/src/commands/ranking.ts +142 -0
- package/src/commands/statistics.ts +158 -0
- package/src/index.ts +41 -0
- package/src/locales/zh-CN.yml +58 -0
- package/src/models/database.ts +84 -0
- package/src/services/api.ts +335 -0
- package/src/services/binding.ts +72 -0
- package/src/services/group-admin.ts +89 -0
- package/src/services/server.ts +93 -0
- package/src/utils/constants.ts +51 -0
- package/src/utils/formatter.ts +172 -0
- package/src/utils/image.ts +114 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/validator.ts +71 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi'
|
|
2
|
+
import { extendDatabase } from './models/database'
|
|
3
|
+
import { ServerConfigService } from './services/server'
|
|
4
|
+
import { QqBindingService } from './services/binding'
|
|
5
|
+
import { GroupAdminService } from './services/group-admin'
|
|
6
|
+
import { registerBindServerCommand } from './commands/bind-server'
|
|
7
|
+
import { registerQueryScoreCommand } from './commands/query-score'
|
|
8
|
+
import { registerBindQqCommand } from './commands/bind-qq'
|
|
9
|
+
import { registerAdjustScoreCommand } from './commands/adjust-score'
|
|
10
|
+
import { registerRankingCommand } from './commands/ranking'
|
|
11
|
+
import { registerStatisticsCommand } from './commands/statistics'
|
|
12
|
+
import { registerGroupAdminCommand } from './commands/admin-manager'
|
|
13
|
+
import { registerMenuCommand } from './commands/menu'
|
|
14
|
+
|
|
15
|
+
export const name = 'class-score-system'
|
|
16
|
+
export const inject = ['database', 'puppeteer']
|
|
17
|
+
|
|
18
|
+
export interface Config {}
|
|
19
|
+
|
|
20
|
+
export const Config: Schema<Config> = Schema.object({})
|
|
21
|
+
|
|
22
|
+
export function apply(ctx: Context) {
|
|
23
|
+
ctx.logger.info('班级操行分管理系统插件正在加载...')
|
|
24
|
+
|
|
25
|
+
extendDatabase(ctx)
|
|
26
|
+
|
|
27
|
+
const serverService = new ServerConfigService(ctx)
|
|
28
|
+
const bindingService = new QqBindingService(ctx)
|
|
29
|
+
const adminService = new GroupAdminService(ctx)
|
|
30
|
+
|
|
31
|
+
registerGroupAdminCommand(ctx, adminService)
|
|
32
|
+
registerBindServerCommand(ctx, serverService, adminService)
|
|
33
|
+
registerQueryScoreCommand(ctx, serverService, bindingService)
|
|
34
|
+
registerBindQqCommand(ctx, serverService, bindingService)
|
|
35
|
+
registerAdjustScoreCommand(ctx, serverService, adminService)
|
|
36
|
+
registerRankingCommand(ctx, serverService)
|
|
37
|
+
registerStatisticsCommand(ctx, serverService, bindingService)
|
|
38
|
+
registerMenuCommand(ctx)
|
|
39
|
+
|
|
40
|
+
ctx.logger.info('班级操行分管理系统插件加载完成')
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
bind-server:
|
|
3
|
+
name: 绑定服务器
|
|
4
|
+
description: 绑定CSMS服务器
|
|
5
|
+
usage: '/绑定服务器 <服务器地址> <管理员用户名> <管理员Token>'
|
|
6
|
+
query-score:
|
|
7
|
+
name: 查询积分
|
|
8
|
+
description: 查询积分和排名
|
|
9
|
+
usage: '/查询积分 [用户名]'
|
|
10
|
+
adjust-score:
|
|
11
|
+
name: 调整积分
|
|
12
|
+
description: 调整积分(仅管理员)
|
|
13
|
+
usage: '/调整积分 <用户名> <分数> <原因>'
|
|
14
|
+
bind-qq:
|
|
15
|
+
name: 绑定QQ
|
|
16
|
+
description: 绑定QQ号与系统账号
|
|
17
|
+
usage: '/绑定QQ <用户名>'
|
|
18
|
+
ranking:
|
|
19
|
+
name: 排行榜
|
|
20
|
+
description: 显示积分排行榜
|
|
21
|
+
usage: '/排行榜 [数量]'
|
|
22
|
+
statistics:
|
|
23
|
+
name: 统计
|
|
24
|
+
description: 显示积分统计信息
|
|
25
|
+
usage: '/统计 [用户名]'
|
|
26
|
+
|
|
27
|
+
messages:
|
|
28
|
+
success:
|
|
29
|
+
server-configured: 服务器配置成功
|
|
30
|
+
server-validated: 服务器配置验证成功
|
|
31
|
+
qq-bound: QQ绑定成功
|
|
32
|
+
qq-unbound: QQ解除绑定成功
|
|
33
|
+
score-adjusted: 积分调整成功
|
|
34
|
+
user-created: 用户创建成功
|
|
35
|
+
error:
|
|
36
|
+
no-server-config: 未配置CSMS服务器,请先使用 /绑定服务器 命令配置
|
|
37
|
+
invalid-token: 无效的Token格式,Token必须由数字和英文大写字母组成
|
|
38
|
+
invalid-qq: 无效的QQ号码格式
|
|
39
|
+
invalid-score: 分数必须在 -1000 到 1000 之间
|
|
40
|
+
invalid-username: 用户名不能为空且长度不能超过50个字符
|
|
41
|
+
invalid-description: 描述信息长度不能超过255个字符
|
|
42
|
+
invalid-limit: 数量必须在 1 到 50 之间
|
|
43
|
+
server-not-found: 服务器未找到
|
|
44
|
+
user-not-found: 用户未找到
|
|
45
|
+
user-already-bound: 该QQ号已绑定其他用户
|
|
46
|
+
user-not-bound: 该QQ号未绑定任何用户
|
|
47
|
+
api-request-failed: API请求失败
|
|
48
|
+
insufficient-permission: 权限不足,仅管理员可执行此操作
|
|
49
|
+
binding-failed: 绑定失败
|
|
50
|
+
unbinding-failed: 解除绑定失败
|
|
51
|
+
configuration-failed: 配置保存失败
|
|
52
|
+
help:
|
|
53
|
+
bind-server: 绑定CSMS服务器\n用法:/绑定服务器 <服务器地址> <管理员用户名> <管理员Token>\n示例:/绑定服务器 https://example.com admin ABC123
|
|
54
|
+
query-score: 查询积分\n用法:/查询积分 [用户名]\n示例:/查询积分 张三
|
|
55
|
+
adjust-score: 调整积分\n用法:/调整积分 <用户名> <分数> <原因>\n示例:/调整积分 张三 +5 表现优秀
|
|
56
|
+
bind-qq: 绑定QQ\n用法:/绑定QQ <用户名>\n示例:/绑定QQ 张三
|
|
57
|
+
ranking: 排行榜\n用法:/排行榜 [数量]\n示例:/排行榜 10
|
|
58
|
+
statistics: 统计\n用法:/统计 [用户名>\n示例:/统计 张三
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Context, Schema, Tables } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface ServerConfig {
|
|
4
|
+
id: number
|
|
5
|
+
guildId: string
|
|
6
|
+
name: string
|
|
7
|
+
address: string
|
|
8
|
+
username: string
|
|
9
|
+
token: string
|
|
10
|
+
createdAt: Date
|
|
11
|
+
updatedAt: Date
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface QqBinding {
|
|
15
|
+
id: number
|
|
16
|
+
guildId: string
|
|
17
|
+
userId: number
|
|
18
|
+
username: string
|
|
19
|
+
openId: string // Koishi session.userId(QQ OpenID,非传统QQ号)
|
|
20
|
+
serverId: number
|
|
21
|
+
createdAt: Date
|
|
22
|
+
updatedAt: Date
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GroupAdmin {
|
|
26
|
+
id: number
|
|
27
|
+
guildId: string
|
|
28
|
+
openId: string // OpenID(用户确认时获取)
|
|
29
|
+
remark: string // 备注(用于减管)
|
|
30
|
+
verify: number // 验证状态:0=待确认, 1=已确认
|
|
31
|
+
createdAt: Date
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module 'koishi' {
|
|
35
|
+
interface Tables {
|
|
36
|
+
server_configs: ServerConfig
|
|
37
|
+
qq_bindings: QqBinding
|
|
38
|
+
group_admins: GroupAdmin
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function extendDatabase(ctx: Context) {
|
|
43
|
+
ctx.model.extend('server_configs', {
|
|
44
|
+
id: 'integer',
|
|
45
|
+
guildId: 'string',
|
|
46
|
+
name: 'string',
|
|
47
|
+
address: 'string',
|
|
48
|
+
username: 'string',
|
|
49
|
+
token: 'string',
|
|
50
|
+
createdAt: 'timestamp',
|
|
51
|
+
updatedAt: 'timestamp',
|
|
52
|
+
}, {
|
|
53
|
+
primary: 'id',
|
|
54
|
+
autoInc: true,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
ctx.model.extend('qq_bindings', {
|
|
58
|
+
id: 'integer',
|
|
59
|
+
guildId: 'string',
|
|
60
|
+
userId: 'integer',
|
|
61
|
+
username: 'string',
|
|
62
|
+
openId: 'string',
|
|
63
|
+
serverId: 'integer',
|
|
64
|
+
createdAt: 'timestamp',
|
|
65
|
+
updatedAt: 'timestamp',
|
|
66
|
+
}, {
|
|
67
|
+
primary: 'id',
|
|
68
|
+
autoInc: true,
|
|
69
|
+
unique: ['openId', 'guildId'],
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
ctx.model.extend('group_admins', {
|
|
73
|
+
id: 'integer',
|
|
74
|
+
guildId: 'string',
|
|
75
|
+
openId: 'string',
|
|
76
|
+
remark: 'string',
|
|
77
|
+
verify: 'integer',
|
|
78
|
+
createdAt: 'timestamp',
|
|
79
|
+
}, {
|
|
80
|
+
primary: 'id',
|
|
81
|
+
autoInc: true,
|
|
82
|
+
unique: ['openId', 'guildId'],
|
|
83
|
+
})
|
|
84
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface CsmsUser {
|
|
4
|
+
id: number
|
|
5
|
+
username: string
|
|
6
|
+
qq_number: string | null
|
|
7
|
+
total_score: number
|
|
8
|
+
add_score: number
|
|
9
|
+
deduct_score: number
|
|
10
|
+
score_count: number
|
|
11
|
+
group_index?: number
|
|
12
|
+
row_index?: number
|
|
13
|
+
col_index?: number
|
|
14
|
+
created_at: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CsmsScoreLog {
|
|
18
|
+
id: number
|
|
19
|
+
user_id: number
|
|
20
|
+
score_change: number
|
|
21
|
+
description: string
|
|
22
|
+
created_at: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AddScoreUser {
|
|
26
|
+
username: string
|
|
27
|
+
score_change: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AddScoreBatchData {
|
|
31
|
+
users: AddScoreUser[]
|
|
32
|
+
description: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AddScoreResult {
|
|
36
|
+
success: boolean
|
|
37
|
+
message: string
|
|
38
|
+
summary: {
|
|
39
|
+
success_count: number
|
|
40
|
+
failed_count: number
|
|
41
|
+
total_count: number
|
|
42
|
+
}
|
|
43
|
+
details: Array<{
|
|
44
|
+
username: string
|
|
45
|
+
user_id?: number
|
|
46
|
+
score_change: number
|
|
47
|
+
success: boolean
|
|
48
|
+
error?: string
|
|
49
|
+
}>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ApiResponse<T = any> {
|
|
53
|
+
data?: T
|
|
54
|
+
error?: string
|
|
55
|
+
success?: boolean
|
|
56
|
+
message?: string
|
|
57
|
+
total?: number
|
|
58
|
+
limit?: number
|
|
59
|
+
offset?: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class CsmsApiService {
|
|
63
|
+
private baseUrl: string
|
|
64
|
+
private token: string
|
|
65
|
+
private ctx: Context
|
|
66
|
+
|
|
67
|
+
constructor(ctx: Context, baseUrl: string, token: string) {
|
|
68
|
+
this.ctx = ctx
|
|
69
|
+
this.baseUrl = baseUrl.replace(/\/api\/global_api\.php$/, '').replace(/\/$/, '')
|
|
70
|
+
this.token = token
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 发送 API 请求
|
|
75
|
+
* 按照 global_api.md 文档规范实现
|
|
76
|
+
*/
|
|
77
|
+
private async request<T>(params: Record<string, string | number | undefined>): Promise<ApiResponse<T>> {
|
|
78
|
+
try {
|
|
79
|
+
const url = `${this.baseUrl}/api/global_api.php`
|
|
80
|
+
// 过滤掉 undefined 值
|
|
81
|
+
const cleanParams: Record<string, string> = {}
|
|
82
|
+
for (const [key, value] of Object.entries(params)) {
|
|
83
|
+
if (value !== undefined && value !== null) {
|
|
84
|
+
cleanParams[key] = String(value)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.ctx.logger.debug(`API请求: ${url}`, cleanParams)
|
|
89
|
+
|
|
90
|
+
const response = await this.ctx.http.get<ApiResponse<T>>(url, {
|
|
91
|
+
params: cleanParams,
|
|
92
|
+
headers: {
|
|
93
|
+
'Authorization': this.token,
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
this.ctx.logger.debug(`API响应:`, response)
|
|
98
|
+
return response
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
this.ctx.logger.error('CSMS API 请求失败:', error)
|
|
101
|
+
return { error: error.message || 'API请求失败' }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 验证 Token - 使用自定义的 add_score 接口测试
|
|
107
|
+
* 注意: 文档未提供专门的 validate_token 接口,用此方法间接验证
|
|
108
|
+
*/
|
|
109
|
+
async validateToken(): Promise<{ valid: boolean; username?: string }> {
|
|
110
|
+
try {
|
|
111
|
+
// 尝试调用 add_score 接口(需要有效token),传入无效数据来检测token是否有效
|
|
112
|
+
const url = `${this.baseUrl}/api/global_api.php`
|
|
113
|
+
const params = {
|
|
114
|
+
action: 'add_score',
|
|
115
|
+
data: JSON.stringify({
|
|
116
|
+
username: '__validate__',
|
|
117
|
+
score_change: 0,
|
|
118
|
+
description: '__token_validation__'
|
|
119
|
+
}),
|
|
120
|
+
token: this.token,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.ctx.logger.info(`验证Token中...`)
|
|
124
|
+
const response = await this.ctx.http.get<ApiResponse>(url, { params })
|
|
125
|
+
|
|
126
|
+
// 如果返回 error: "用户不存在" 或类似,说明 token 有效
|
|
127
|
+
// 如果返回 error: "未授权" 或 "Token无效",说明 token 无效
|
|
128
|
+
if (response.error) {
|
|
129
|
+
const errorLower = response.error.toLowerCase()
|
|
130
|
+
if (errorLower.includes('未授权') || errorLower.includes('token') || errorLower.includes('无效')) {
|
|
131
|
+
this.ctx.logger.warn(`Token验证失败: ${response.error}`)
|
|
132
|
+
return { valid: false }
|
|
133
|
+
}
|
|
134
|
+
// 其他错误(如用户不存在)说明token有效
|
|
135
|
+
this.ctx.logger.info(`Token验证成功`)
|
|
136
|
+
return { valid: true }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { valid: true }
|
|
140
|
+
} catch (error: any) {
|
|
141
|
+
this.ctx.logger.error(`Token验证异常:`, error)
|
|
142
|
+
return { valid: false }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 查询用户列表
|
|
148
|
+
* GET /api/global_api.php?users&order_by=xxx&order=DESC&limit=20&offset=0
|
|
149
|
+
*/
|
|
150
|
+
async getUsers(options?: {
|
|
151
|
+
where?: Record<string, any>
|
|
152
|
+
order_by?: string
|
|
153
|
+
order?: 'ASC' | 'DESC'
|
|
154
|
+
limit?: number
|
|
155
|
+
offset?: number
|
|
156
|
+
search?: string
|
|
157
|
+
}): Promise<ApiResponse<CsmsUser[]>> {
|
|
158
|
+
const params: Record<string, string> = { users: '' }
|
|
159
|
+
|
|
160
|
+
if (options?.where) {
|
|
161
|
+
params.where = JSON.stringify(options.where)
|
|
162
|
+
}
|
|
163
|
+
if (options?.order_by) {
|
|
164
|
+
params.order_by = options.order_by
|
|
165
|
+
}
|
|
166
|
+
if (options?.order) {
|
|
167
|
+
params.order = options.order
|
|
168
|
+
}
|
|
169
|
+
if (options?.limit !== undefined) {
|
|
170
|
+
params.limit = options.limit.toString()
|
|
171
|
+
}
|
|
172
|
+
if (options?.offset !== undefined) {
|
|
173
|
+
params.offset = options.offset.toString()
|
|
174
|
+
}
|
|
175
|
+
if (options?.search) {
|
|
176
|
+
params.search = options.search
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.request<CsmsUser[]>(params)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 查询单个用户
|
|
184
|
+
* GET /api/global_api.php?users&id=xxx
|
|
185
|
+
*/
|
|
186
|
+
async getUserById(id: number): Promise<ApiResponse<CsmsUser[]>> {
|
|
187
|
+
return this.request<CsmsUser[]>({ users: '', id: id.toString() })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 按用户名查询用户
|
|
192
|
+
* GET /api/global_api.php?users&where={"username":"xxx"}
|
|
193
|
+
*/
|
|
194
|
+
async getUserByUsername(username: string): Promise<ApiResponse<CsmsUser[]>> {
|
|
195
|
+
return this.request<CsmsUser[]>({
|
|
196
|
+
users: '',
|
|
197
|
+
where: JSON.stringify({ username })
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 按QQ号查询用户
|
|
203
|
+
* GET /api/global_api.php?users&where={"qq_number":"xxx"}
|
|
204
|
+
*/
|
|
205
|
+
async getUserByQq(qqNumber: string): Promise<ApiResponse<CsmsUser[]>> {
|
|
206
|
+
return this.request<CsmsUser[]>({
|
|
207
|
+
users: '',
|
|
208
|
+
where: JSON.stringify({ qq_number: qqNumber })
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 获取排行榜(按 total_score 降序)
|
|
214
|
+
* GET /api/global_api.php?users&order_by=total_score&order=DESC&limit=20
|
|
215
|
+
*/
|
|
216
|
+
async getRanking(limit: number = 20, offset: number = 0): Promise<ApiResponse<CsmsUser[]>> {
|
|
217
|
+
return this.request<CsmsUser[]>({
|
|
218
|
+
users: '',
|
|
219
|
+
order_by: 'total_score',
|
|
220
|
+
order: 'DESC',
|
|
221
|
+
limit: limit.toString(),
|
|
222
|
+
offset: offset.toString(),
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 查询积分记录
|
|
228
|
+
* GET /api/global_api.php?score_logs&where={"user_id":xxx}&order_by=created_at&order=DESC&limit=50
|
|
229
|
+
*/
|
|
230
|
+
async getScoreLogs(userId: number, limit: number = 50): Promise<ApiResponse<CsmsScoreLog[]>> {
|
|
231
|
+
return this.request<CsmsScoreLog[]>({
|
|
232
|
+
score_logs: '',
|
|
233
|
+
where: JSON.stringify({ user_id: userId }),
|
|
234
|
+
order_by: 'created_at',
|
|
235
|
+
order: 'DESC',
|
|
236
|
+
limit: limit.toString(),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 创建新用户
|
|
242
|
+
* GET /api/global_api.php?users&action=create&data={"username":"xxx"}&token=xxx
|
|
243
|
+
*/
|
|
244
|
+
async createUser(username: string): Promise<ApiResponse<CsmsUser>> {
|
|
245
|
+
return this.request<CsmsUser>({
|
|
246
|
+
users: '',
|
|
247
|
+
action: 'create',
|
|
248
|
+
data: JSON.stringify({ username }),
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 更新用户信息(如绑定QQ号)
|
|
254
|
+
* GET /api/global_api.php?users&action=update&id=xxx&data={"qq_number":"xxx"}&token=xxx
|
|
255
|
+
*/
|
|
256
|
+
async updateUser(userId: number, data: Record<string, any>): Promise<ApiResponse<CsmsUser>> {
|
|
257
|
+
return this.request<CsmsUser>({
|
|
258
|
+
users: '',
|
|
259
|
+
action: 'update',
|
|
260
|
+
id: userId.toString(),
|
|
261
|
+
data: JSON.stringify(data),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 绑定用户QQ号
|
|
267
|
+
*/
|
|
268
|
+
async bindQq(userId: number, qqNumber: string): Promise<ApiResponse<CsmsUser>> {
|
|
269
|
+
return this.updateUser(userId, { qq_number: qqNumber })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 批量加减分
|
|
274
|
+
* GET /api/global_api.php?action=add_score&data={"users":[...],"description":"xxx"}&token=xxx
|
|
275
|
+
*/
|
|
276
|
+
async batchAddScore(data: AddScoreBatchData): Promise<AddScoreResult> {
|
|
277
|
+
this.ctx.logger.info(`batchAddScore 请求数据:`, JSON.stringify(data))
|
|
278
|
+
|
|
279
|
+
const response = await this.request<AddScoreResult>({
|
|
280
|
+
action: 'add_score',
|
|
281
|
+
data: JSON.stringify(data),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
this.ctx.logger.info(`batchAddScore API 响应:`, JSON.stringify(response))
|
|
285
|
+
|
|
286
|
+
// 检查错误
|
|
287
|
+
if (response.error) {
|
|
288
|
+
return {
|
|
289
|
+
success: false,
|
|
290
|
+
message: response.error,
|
|
291
|
+
summary: { success_count: 0, failed_count: 0, total_count: 0 },
|
|
292
|
+
details: [],
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// CSMS API 直接返回 AddScoreResult 对象,不在 data 字段中
|
|
297
|
+
// 检查 response 是否就是 AddScoreResult
|
|
298
|
+
if (response.data && typeof response.data === 'object' && 'summary' in response.data) {
|
|
299
|
+
return response.data as AddScoreResult
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 兼容:检查 response 本身是否就是 AddScoreResult 格式
|
|
303
|
+
if (response && typeof response === 'object' && 'summary' in response) {
|
|
304
|
+
return response as AddScoreResult
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 检查 response.data 是否为数组形式(兼容某些情况)
|
|
308
|
+
if (Array.isArray(response.data)) {
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
message: '操作完成',
|
|
312
|
+
summary: { success_count: response.data.length, failed_count: 0, total_count: response.data.length },
|
|
313
|
+
details: response.data,
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.ctx.logger.error(`batchAddScore 返回数据格式异常,原始响应:`, response)
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
message: 'API 返回数据格式异常',
|
|
321
|
+
summary: { success_count: 0, failed_count: 0, total_count: 0 },
|
|
322
|
+
details: [],
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 单用户加减分
|
|
328
|
+
*/
|
|
329
|
+
async addScore(username: string, scoreChange: number, description: string): Promise<AddScoreResult> {
|
|
330
|
+
return this.batchAddScore({
|
|
331
|
+
users: [{ username, score_change: scoreChange }],
|
|
332
|
+
description,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import { QqBinding } from '../models/database'
|
|
3
|
+
|
|
4
|
+
export class QqBindingService {
|
|
5
|
+
private ctx: Context
|
|
6
|
+
|
|
7
|
+
constructor(ctx: Context) {
|
|
8
|
+
this.ctx = ctx
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* openId 即 session.userId(QQ OpenID,非传统QQ号)
|
|
13
|
+
*/
|
|
14
|
+
async bindQq(guildId: string, userId: number, username: string, openId: string, serverId: number): Promise<QqBinding> {
|
|
15
|
+
const now = new Date()
|
|
16
|
+
|
|
17
|
+
const bindings = await this.ctx.database.get('qq_bindings', { openId, guildId })
|
|
18
|
+
if (bindings.length > 0) {
|
|
19
|
+
await this.ctx.database.set('qq_bindings', { openId, guildId }, {
|
|
20
|
+
userId,
|
|
21
|
+
username,
|
|
22
|
+
serverId,
|
|
23
|
+
updatedAt: now,
|
|
24
|
+
})
|
|
25
|
+
return { ...bindings[0], userId, username, serverId, updatedAt: now }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const binding = await this.ctx.database.create('qq_bindings', {
|
|
29
|
+
guildId,
|
|
30
|
+
userId,
|
|
31
|
+
username,
|
|
32
|
+
openId,
|
|
33
|
+
serverId,
|
|
34
|
+
createdAt: now,
|
|
35
|
+
updatedAt: now,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return binding
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async unbindQq(guildId: string, openId: string): Promise<boolean> {
|
|
42
|
+
await this.ctx.database.remove('qq_bindings', { openId, guildId })
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async unbindByUserId(guildId: string, userId: number): Promise<boolean> {
|
|
47
|
+
await this.ctx.database.remove('qq_bindings', { userId, guildId })
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getUserByOpenId(guildId: string, openId: string): Promise<QqBinding | null> {
|
|
52
|
+
const bindings = await this.ctx.database.get('qq_bindings', { openId, guildId })
|
|
53
|
+
return bindings[0] || null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getQqByUserId(guildId: string, userId: number): Promise<QqBinding | null> {
|
|
57
|
+
const bindings = await this.ctx.database.get('qq_bindings', { userId, guildId })
|
|
58
|
+
return bindings[0] || null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getAllBindings(guildId: string): Promise<QqBinding[]> {
|
|
62
|
+
return this.ctx.database.get('qq_bindings', { guildId })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getBindingsByServer(guildId: string, serverId: number): Promise<QqBinding[]> {
|
|
66
|
+
return this.ctx.database.get('qq_bindings', { serverId, guildId })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getBindingsByUsername(guildId: string, username: string): Promise<QqBinding[]> {
|
|
70
|
+
return this.ctx.database.get('qq_bindings', { username, guildId })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import { GroupAdmin } from '../models/database'
|
|
3
|
+
|
|
4
|
+
export class GroupAdminService {
|
|
5
|
+
private ctx: Context
|
|
6
|
+
|
|
7
|
+
constructor(ctx: Context) {
|
|
8
|
+
this.ctx = ctx
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 添加已验证的管理员(verify = 1,用于绑定服务器时自动升级)
|
|
13
|
+
*/
|
|
14
|
+
async addAdmin(guildId: string, openId: string, remark: string): Promise<GroupAdmin> {
|
|
15
|
+
const existing = await this.ctx.database.get('group_admins', { guildId, openId })
|
|
16
|
+
if (existing.length > 0) {
|
|
17
|
+
// 已存在则更新为已验证
|
|
18
|
+
await this.ctx.database.set('group_admins', { guildId, openId }, { remark, verify: 1 })
|
|
19
|
+
return { ...existing[0], remark, verify: 1 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return this.ctx.database.create('group_admins', {
|
|
23
|
+
guildId,
|
|
24
|
+
openId,
|
|
25
|
+
remark,
|
|
26
|
+
verify: 1,
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 创建待确认的群管记录(Verify = 0)
|
|
33
|
+
*/
|
|
34
|
+
async createPendingAdmin(guildId: string, openId: string, remark: string): Promise<GroupAdmin> {
|
|
35
|
+
// 检查是否已存在(按 openId 查)
|
|
36
|
+
const existing = await this.ctx.database.get('group_admins', { guildId, openId })
|
|
37
|
+
if (existing.length > 0) {
|
|
38
|
+
return existing[0]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.ctx.database.create('group_admins', {
|
|
42
|
+
guildId,
|
|
43
|
+
openId,
|
|
44
|
+
remark,
|
|
45
|
+
verify: 0,
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 确认群管(将 Verify 改为 1)
|
|
52
|
+
*/
|
|
53
|
+
async confirmAdmin(guildId: string, openId: string): Promise<boolean> {
|
|
54
|
+
await this.ctx.database.set('group_admins', { guildId, openId }, { verify: 1 })
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 移除群管(按备注 查找并删除)
|
|
60
|
+
*/
|
|
61
|
+
async removeAdminByRemark(guildId: string, remark: string): Promise<boolean> {
|
|
62
|
+
await this.ctx.database.remove('group_admins', { guildId, remark })
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 检查是否为已确认的群管(OpenID + Verify = 1)
|
|
68
|
+
*/
|
|
69
|
+
async isAdmin(guildId: string, openId: string): Promise<boolean> {
|
|
70
|
+
const admins = await this.ctx.database.get('group_admins', { guildId, openId })
|
|
71
|
+
return admins.length > 0 && admins[0].verify === 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 获取所有群管
|
|
76
|
+
*/
|
|
77
|
+
async getAllAdmins(guildId: string): Promise<GroupAdmin[]> {
|
|
78
|
+
return this.ctx.database.get('group_admins', { guildId })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 检查用户是否有管理权限(按 OpenID + Verify = 1)
|
|
83
|
+
*/
|
|
84
|
+
async hasAdminPermission(session: any): Promise<boolean> {
|
|
85
|
+
const openId = session.userId?.toString()
|
|
86
|
+
if (!openId || !session.guildId) return false
|
|
87
|
+
return await this.isAdmin(session.guildId, openId)
|
|
88
|
+
}
|
|
89
|
+
}
|