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/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
+ }