mirror of https://github.com/tp4a/teleport
temp.
parent
39ced04d25
commit
1db8db69c6
|
@ -829,6 +829,7 @@ $app.create_dlg_edit_host = function () {
|
||||||
dlg.dom.edit_router_port.val('' + dlg.field_router_port);
|
dlg.dom.edit_router_port.val('' + dlg.field_router_port);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
dlg.field_router_ip = '';
|
||||||
dlg.field_router_port = 0;
|
dlg.field_router_port = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -878,88 +879,6 @@ $app.create_dlg_edit_host = function () {
|
||||||
return dlg;
|
return dlg;
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.create_dlg_host_info = function () {
|
|
||||||
var dlg = {};
|
|
||||||
dlg.dom_id = 'dlg-user-info';
|
|
||||||
dlg.row_id = -1;
|
|
||||||
dlg.need_edit = false;
|
|
||||||
|
|
||||||
dlg.dom = {
|
|
||||||
dialog: $('#' + dlg.dom_id),
|
|
||||||
dlg_title: $('#' + dlg.dom_id + ' [data-field="dlg-title"]'),
|
|
||||||
info: $('#' + dlg.dom_id + ' [data-field="user-info"]'),
|
|
||||||
btn_edit: $('#' + dlg.dom_id + ' [data-field="btn-edit"]')
|
|
||||||
};
|
|
||||||
|
|
||||||
dlg.init = function (cb_stack) {
|
|
||||||
dlg.dom.dialog.on('hidden.bs.modal', function () {
|
|
||||||
if (!dlg.need_edit)
|
|
||||||
return;
|
|
||||||
$app.dlg_edit_user.show_edit(dlg.row_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
dlg.dom.btn_edit.click(function () {
|
|
||||||
dlg.need_edit = true;
|
|
||||||
dlg.dom.dialog.modal('hide');
|
|
||||||
});
|
|
||||||
|
|
||||||
cb_stack.exec();
|
|
||||||
};
|
|
||||||
|
|
||||||
dlg.show = function (row_id) {
|
|
||||||
dlg.row_id = row_id;
|
|
||||||
dlg.need_edit = false;
|
|
||||||
|
|
||||||
var _row_data = $app.table_host.get_row(dlg.row_id);
|
|
||||||
|
|
||||||
// 表格加载时,是不会读取用户的 desc 字段的,因此可以判断此用户是否已经读取过详细信息了
|
|
||||||
if (_.isUndefined(_row_data.desc)) {
|
|
||||||
// 尚未读取,则向服务器要求获取此用户账号的完整信息
|
|
||||||
$tp.ajax_post_json('/user/get-user/' + _row_data.id, {},
|
|
||||||
function (ret) {
|
|
||||||
if (ret.code === TPE_OK) {
|
|
||||||
$app.table_host.update_row(dlg.row_id, ret.data);
|
|
||||||
dlg.show_info(ret.data);
|
|
||||||
} else {
|
|
||||||
$tp.notify_error('无法获取用户详细信息:' + tp_error_msg(ret.code, ret.message));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
$tp.notify_error('网络故障,无法获取用户详细信息!');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dlg.show_info(_row_data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dlg.show_info = function (user) {
|
|
||||||
// 更新对话框中显示的信息
|
|
||||||
dlg.dom.dlg_title.html('<i class="fa fa-vcard-o fa-fw"></i> ' + user.surname);
|
|
||||||
|
|
||||||
var info = [];
|
|
||||||
|
|
||||||
var not_set = '<span class="label label-sm label-ignore">未设置</span>';
|
|
||||||
var mobile = (user.mobile.length === 0) ? not_set : user.mobile;
|
|
||||||
var qq = (user.qq.length === 0) ? not_set : user.qq;
|
|
||||||
var wechat = (user.wechat.length === 0) ? not_set : user.wechat;
|
|
||||||
var desc = (user.desc.length === 0) ? not_set : user.desc;
|
|
||||||
info.push('<tr><td class="key">账号:</td><td class="value">' + user.username + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">姓名:</td><td class="value">' + user.surname + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">邮箱:</td><td class="value">' + user.email + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">电话:</td><td class="value">' + mobile + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">QQ:</td><td class="value">' + qq + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">微信:</td><td class="value">' + wechat + '</td></tr>');
|
|
||||||
info.push('<tr><td class="key">描述:</td><td class="value"><div style="max-height:80px;overflow:auto;">' + desc + '</div></td></tr>');
|
|
||||||
|
|
||||||
dlg.dom.info.html($(info.join('')));
|
|
||||||
|
|
||||||
dlg.dom.dialog.modal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return dlg;
|
|
||||||
};
|
|
||||||
|
|
||||||
$app.create_dlg_accounts = function () {
|
$app.create_dlg_accounts = function () {
|
||||||
var dlg = {};
|
var dlg = {};
|
||||||
dlg.dom_id = 'dlg-accounts';
|
dlg.dom_id = 'dlg-accounts';
|
||||||
|
|
|
@ -486,7 +486,7 @@ class AppConfig(BaseAppConfig):
|
||||||
|
|
||||||
self.sys.login = tp_convert_to_attr_dict(_login)
|
self.sys.login = tp_convert_to_attr_dict(_login)
|
||||||
if not self.sys.login.is_exists('session_timeout'):
|
if not self.sys.login.is_exists('session_timeout'):
|
||||||
self.sys.login.session_timeout = 30
|
self.sys.login.session_timeout = 60 # 1 hour
|
||||||
if not self.sys.login.is_exists('retry'):
|
if not self.sys.login.is_exists('retry'):
|
||||||
self.sys.login.retry = 0
|
self.sys.login.retry = 0
|
||||||
if not self.sys.login.is_exists('lock_timeout'):
|
if not self.sys.login.is_exists('lock_timeout'):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from app.base.logger import log
|
from app.base.logger import log
|
||||||
|
from app.base.configs import get_cfg
|
||||||
|
|
||||||
|
|
||||||
class SessionManager(threading.Thread):
|
class SessionManager(threading.Thread):
|
||||||
|
@ -62,7 +63,8 @@ class SessionManager(threading.Thread):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if expire is None:
|
if expire is None:
|
||||||
expire = self.SESSION_EXPIRE
|
# expire = self.SESSION_EXPIRE
|
||||||
|
expire = get_cfg().sys.login.session_timeout * 60
|
||||||
|
|
||||||
if expire < 0:
|
if expire < 0:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|
|
@ -9,13 +9,6 @@ TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH = 0x0008 # 用户名+密码+OATH
|
||||||
APP_MODE_NORMAL = 1
|
APP_MODE_NORMAL = 1
|
||||||
APP_MODE_MAINTENANCE = 2
|
APP_MODE_MAINTENANCE = 2
|
||||||
|
|
||||||
# ==========================================================================
|
|
||||||
# 用户账号状态
|
|
||||||
# ==========================================================================
|
|
||||||
USER_STATE_NORMAL = 1 # 正常状态
|
|
||||||
USER_STATE_LOCKED = 2 # 锁定(例如密码连错n次)
|
|
||||||
USER_STATE_DISABLED = 3 # 禁用
|
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
# 远程主机账号认证方式
|
# 远程主机账号认证方式
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|
|
@ -11,6 +11,7 @@ from app.logic.auth.captcha import tp_captcha_generate_image
|
||||||
from app.model import user
|
from app.model import user
|
||||||
from app.model import syslog
|
from app.model import syslog
|
||||||
from app.logic.auth.password import tp_password_verify
|
from app.logic.auth.password import tp_password_verify
|
||||||
|
from app.base.utils import tp_timestamp_utc_now
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(TPBaseHandler):
|
class LoginHandler(TPBaseHandler):
|
||||||
|
@ -53,68 +54,86 @@ class LoginHandler(TPBaseHandler):
|
||||||
|
|
||||||
class DoLoginHandler(TPBaseJsonHandler):
|
class DoLoginHandler(TPBaseJsonHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
|
sys_cfg = get_cfg().sys
|
||||||
|
|
||||||
args = self.get_argument('args', None)
|
args = self.get_argument('args', None)
|
||||||
if args is not None:
|
if args is None:
|
||||||
try:
|
return self.write_json(TPE_PARAM)
|
||||||
args = json.loads(args)
|
|
||||||
except:
|
|
||||||
return self.write_json(TPE_JSON_FORMAT, '参数错误')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
login_type = args['type']
|
args = json.loads(args)
|
||||||
captcha = args['captcha'].strip()
|
except:
|
||||||
username = args['username'].strip()
|
return self.write_json(TPE_JSON_FORMAT, '参数错误')
|
||||||
password = args['password']
|
|
||||||
oath = args['oath'].strip()
|
|
||||||
remember = args['remember']
|
|
||||||
except:
|
|
||||||
return self.write_json(TPE_PARAM, '参数错误')
|
|
||||||
else:
|
|
||||||
return self.write_json(TPE_PARAM, '参数错误')
|
|
||||||
|
|
||||||
_tmp = {'username': username, 'surname': username}
|
try:
|
||||||
|
login_type = args['type']
|
||||||
|
captcha = args['captcha'].strip()
|
||||||
|
username = args['username'].strip().lower()
|
||||||
|
password = args['password']
|
||||||
|
oath = args['oath'].strip()
|
||||||
|
remember = args['remember']
|
||||||
|
except:
|
||||||
|
return self.write_json(TPE_PARAM)
|
||||||
|
|
||||||
|
if login_type not in [TP_LOGIN_AUTH_USERNAME_PASSWORD,
|
||||||
|
TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA,
|
||||||
|
TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH,
|
||||||
|
TP_LOGIN_AUTH_USERNAME_OATH
|
||||||
|
]:
|
||||||
|
return self.write_json(TPE_PARAM, '未知的认证方式')
|
||||||
|
|
||||||
if login_type == TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA:
|
if login_type == TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA:
|
||||||
oath = None
|
oath = None
|
||||||
code = self.get_session('captcha')
|
code = self.get_session('captcha')
|
||||||
if code is None:
|
if code is None:
|
||||||
return self.write_json(TPE_CAPTCHA_EXPIRED, '验证码已失效')
|
return self.write_json(TPE_CAPTCHA_EXPIRED, '验证码已失效')
|
||||||
self.del_session('captcha')
|
|
||||||
if code.lower() != captcha.lower():
|
if code.lower() != captcha.lower():
|
||||||
return self.write_json(TPE_CAPTCHA_MISMATCH, '验证码错误')
|
return self.write_json(TPE_CAPTCHA_MISMATCH, '验证码错误')
|
||||||
elif login_type == TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH:
|
elif login_type in [TP_LOGIN_AUTH_USERNAME_OATH, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
|
||||||
if len(oath) == 0:
|
if len(oath) == 0:
|
||||||
return self.write_json(TPE_OATH_MISMATCH, '未提供身份验证器动态验证码')
|
return self.write_json(TPE_OATH_MISMATCH, '未提供身份验证器动态验证码')
|
||||||
else:
|
|
||||||
return self.write_json(TPE_PARAM, '参数错误')
|
|
||||||
|
|
||||||
self.del_session('captcha')
|
self.del_session('captcha')
|
||||||
|
|
||||||
|
if len(username) == 0:
|
||||||
|
return self.write_json(TPE_PARAM, '未提供登录用户名')
|
||||||
|
|
||||||
err, user_info = user.get_by_username(username)
|
err, user_info = user.get_by_username(username)
|
||||||
# if user_info is None:
|
|
||||||
# return self.write_json(TPE_USER_AUTH)
|
|
||||||
if err != TPE_OK:
|
if err != TPE_OK:
|
||||||
if err == TPE_NOT_EXISTS:
|
if err == TPE_NOT_EXISTS:
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_NOT_EXISTS, '登录失败,用户`{}`不存在'.format(username))
|
syslog.sys_log({'username': username, 'surname': username}, self.request.remote_ip, TPE_NOT_EXISTS, '登录失败,用户`{}`不存在'.format(username))
|
||||||
return self.write_json(err)
|
return self.write_json(err)
|
||||||
|
|
||||||
if user_info['state'] == USER_STATE_LOCKED:
|
if user_info['state'] == TP_STATE_LOCKED:
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_USER_LOCKED, '登录失败,用户已被锁定')
|
# 用户已经被锁定,如果系统配置为一定时间后自动解锁,则更新一下用户信息
|
||||||
return self.write_json(TPE_USER_LOCKED)
|
if sys_cfg.login.lock_timeout != 0:
|
||||||
elif user_info['state'] == USER_STATE_DISABLED:
|
if tp_timestamp_utc_now() - user_info.lock_time > sys_cfg.login.lock_timeout * 60:
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_USER_DISABLED, '登录失败,用户已被禁用')
|
user_info.fail_count = 0
|
||||||
|
user_info.state = TP_STATE_NORMAL
|
||||||
|
if user_info['state'] == TP_STATE_LOCKED:
|
||||||
|
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_LOCKED, '登录失败,用户已被锁定')
|
||||||
|
return self.write_json(TPE_USER_LOCKED)
|
||||||
|
elif user_info['state'] == TP_STATE_DISABLED:
|
||||||
|
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_DISABLED, '登录失败,用户已被禁用')
|
||||||
return self.write_json(TPE_USER_DISABLED)
|
return self.write_json(TPE_USER_DISABLED)
|
||||||
elif user_info['state'] != USER_STATE_NORMAL:
|
elif user_info['state'] != TP_STATE_NORMAL:
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_FAILED, '登录失败,系统内部错误')
|
syslog.sys_log(user_info, self.request.remote_ip, TPE_FAILED, '登录失败,系统内部错误')
|
||||||
return self.write_json(TPE_FAILED)
|
return self.write_json(TPE_FAILED)
|
||||||
|
|
||||||
if login_type == TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA:
|
err_msg = ''
|
||||||
|
if login_type in [TP_LOGIN_AUTH_USERNAME_PASSWORD, TP_LOGIN_AUTH_USERNAME_PASSWORD_CAPTCHA, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
|
||||||
if not tp_password_verify(password, user_info['password']):
|
if not tp_password_verify(password, user_info['password']):
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_USER_AUTH, '登录失败,密码错误!')
|
err, is_locked = user.update_fail_count(self, user_info)
|
||||||
|
if is_locked:
|
||||||
|
err_msg = '用户被临时锁定!'
|
||||||
|
syslog.sys_log(user_info, self.request.remote_ip, TPE_USER_AUTH, '登录失败,密码错误!{}'.format(err_msg))
|
||||||
return self.write_json(TPE_USER_AUTH)
|
return self.write_json(TPE_USER_AUTH)
|
||||||
elif login_type == TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH:
|
elif login_type in [TP_LOGIN_AUTH_USERNAME_OATH, TP_LOGIN_AUTH_USERNAME_PASSWORD_OATH]:
|
||||||
if not tp_oath_verify_code(user_info['oath_secret'], oath):
|
if not tp_oath_verify_code(user_info['oath_secret'], oath):
|
||||||
syslog.sys_log(_tmp, self.request.remote_ip, TPE_OATH_MISMATCH, "登录失败,身份验证器动态验证码错误!")
|
err, is_locked = user.update_fail_count(self, user_info)
|
||||||
|
if is_locked:
|
||||||
|
err_msg = '用户被临时锁定!'
|
||||||
|
syslog.sys_log(user_info, self.request.remote_ip, TPE_OATH_MISMATCH, "登录失败,身份验证器动态验证码错误!{}".format(err_msg))
|
||||||
return self.write_json(TPE_OATH_MISMATCH)
|
return self.write_json(TPE_OATH_MISMATCH)
|
||||||
|
|
||||||
self._user = user_info
|
self._user = user_info
|
||||||
|
@ -122,12 +141,9 @@ class DoLoginHandler(TPBaseJsonHandler):
|
||||||
del self._user['password']
|
del self._user['password']
|
||||||
del self._user['oath_secret']
|
del self._user['oath_secret']
|
||||||
|
|
||||||
print('00000', self._user)
|
|
||||||
|
|
||||||
if remember:
|
if remember:
|
||||||
self.set_session('user', self._user, 12 * 60 * 60)
|
self.set_session('user', self._user, 12 * 60 * 60)
|
||||||
else:
|
else:
|
||||||
# TODO: 使用系统配置项中的默认会话超时
|
|
||||||
self.set_session('user', self._user)
|
self.set_session('user', self._user)
|
||||||
|
|
||||||
user.update_login_info(self, user_info['id'])
|
user.update_login_info(self, user_info['id'])
|
||||||
|
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
# import hashlib
|
# import hashlib
|
||||||
|
|
||||||
from app.const import *
|
from app.base.configs import get_cfg
|
||||||
from app.base.logger import log
|
|
||||||
# from app.base.configs import get_cfg
|
|
||||||
from app.base.db import get_db, SQL
|
from app.base.db import get_db, SQL
|
||||||
from app.model import syslog
|
from app.base.logger import log
|
||||||
# from app.logic.auth.oath import tp_oath_verify_code
|
|
||||||
# from app.logic.auth.password import tp_password_generate_secret, tp_password_verify
|
|
||||||
from app.base.utils import tp_timestamp_utc_now
|
from app.base.utils import tp_timestamp_utc_now
|
||||||
|
from app.const import *
|
||||||
|
from app.model import syslog
|
||||||
|
|
||||||
|
|
||||||
def get_user_info(user_id):
|
def get_user_info(user_id):
|
||||||
|
@ -32,7 +30,7 @@ def get_user_info(user_id):
|
||||||
|
|
||||||
def get_by_username(username):
|
def get_by_username(username):
|
||||||
s = SQL(get_db())
|
s = SQL(get_db())
|
||||||
s.select_from('user', ['id', 'type', 'auth_type', 'username', 'surname', 'password', 'oath_secret', 'role_id', 'state', 'email', 'create_time', 'last_login', 'last_ip', 'last_chpass', 'mobile', 'qq', 'wechat', 'desc'], alt_name='u')
|
s.select_from('user', ['id', 'type', 'auth_type', 'username', 'surname', 'password', 'oath_secret', 'role_id', 'state', 'fail_count', 'lock_time', 'email', 'create_time', 'last_login', 'last_ip', 'last_chpass', 'mobile', 'qq', 'wechat', 'desc'], alt_name='u')
|
||||||
s.left_join('role', ['name', 'privilege'], join_on='r.id=u.role_id', alt_name='r', out_map={'name': 'role'})
|
s.left_join('role', ['name', 'privilege'], join_on='r.id=u.role_id', alt_name='r', out_map={'name': 'role'})
|
||||||
s.where('u.username="{}"'.format(username))
|
s.where('u.username="{}"'.format(username))
|
||||||
err = s.query()
|
err = s.query()
|
||||||
|
@ -249,11 +247,10 @@ def set_password(handler, user_id, password):
|
||||||
|
|
||||||
|
|
||||||
def update_login_info(handler, user_id):
|
def update_login_info(handler, user_id):
|
||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
_time_now = tp_timestamp_utc_now()
|
_time_now = tp_timestamp_utc_now()
|
||||||
|
|
||||||
sql = 'UPDATE `{}user` SET last_login=login_time, last_ip=login_ip, login_time={login_time}, login_ip="{ip}" WHERE id={user_id};' \
|
sql = 'UPDATE `{}user` SET fail_count=0, last_login=login_time, last_ip=login_ip, login_time={login_time}, login_ip="{ip}" WHERE id={user_id};' \
|
||||||
''.format(db.table_prefix,
|
''.format(db.table_prefix,
|
||||||
login_time=_time_now, ip=handler.request.remote_ip, user_id=user_id
|
login_time=_time_now, ip=handler.request.remote_ip, user_id=user_id
|
||||||
)
|
)
|
||||||
|
@ -285,6 +282,29 @@ def update_users_state(handler, user_ids, state):
|
||||||
return TPE_DATABASE
|
return TPE_DATABASE
|
||||||
|
|
||||||
|
|
||||||
|
def update_fail_count(handler, user_info):
|
||||||
|
db = get_db()
|
||||||
|
sys_cfg = get_cfg().sys
|
||||||
|
sql_list = []
|
||||||
|
is_locked = False
|
||||||
|
fail_count = user_info.fail_count + 1
|
||||||
|
|
||||||
|
sql = 'UPDATE `{}user` SET fail_count={count} WHERE id={uid};' \
|
||||||
|
''.format(db.table_prefix, count=fail_count, uid=user_info.id)
|
||||||
|
sql_list.append(sql)
|
||||||
|
|
||||||
|
if sys_cfg.login.retry != 0 and fail_count >= sys_cfg.login.retry:
|
||||||
|
is_locked = True
|
||||||
|
sql = 'UPDATE `{}user` SET state={state}, lock_time={lock_time} WHERE id={uid};' \
|
||||||
|
''.format(db.table_prefix, state=TP_STATE_LOCKED, lock_time=tp_timestamp_utc_now(), uid=user_info.id)
|
||||||
|
sql_list.append(sql)
|
||||||
|
|
||||||
|
if db.transaction(sql_list):
|
||||||
|
return TPE_OK, is_locked
|
||||||
|
else:
|
||||||
|
return TPE_DATABASE, is_locked
|
||||||
|
|
||||||
|
|
||||||
def remove_users(handler, users):
|
def remove_users(handler, users):
|
||||||
s = SQL(get_db())
|
s = SQL(get_db())
|
||||||
|
|
||||||
|
@ -587,7 +607,7 @@ def get_group_with_member(sql_filter, sql_order, sql_limit):
|
||||||
for g in sg.recorder:
|
for g in sg.recorder:
|
||||||
g['member_count'] = 0
|
g['member_count'] = 0
|
||||||
g['members'] = []
|
g['members'] = []
|
||||||
g['_mid'] = [] # 临时使用,构建此组的前5个成员的id
|
g['_mid'] = [] # 临时使用,构建此组的前5个成员的id
|
||||||
|
|
||||||
# 对于本次要返回的用户组,取其中每一个组内成员的基本信息(id/用户名/真实名称等)
|
# 对于本次要返回的用户组,取其中每一个组内成员的基本信息(id/用户名/真实名称等)
|
||||||
groups = [g['id'] for g in sg.recorder]
|
groups = [g['id'] for g in sg.recorder]
|
||||||
|
|
Loading…
Reference in New Issue