feat: LDAP 账号管理 — OU管理/添加用户/删除用户/批量导入

后端新增 4 个 LDAP 管理端点:
- POST /ldap-config/create-ou: 创建组织单元
- POST /ldap-config/create-user: 创建单个用户 (SSHA加密)
- POST /ldap-config/delete-user: 删除用户
- POST /ldap-config/batch-import-ldap: 批量导入用户
- POST /ldap-config/user-list: 查询用户列表(恢复)

前端 LdapConfigPage 增强:
- LDAP 账号管理折叠区(配置存在即显示)
- OU 下拉选择 + 根节点选项 + 数量统计
- 用户列表表格(全选/单选/DN显示)
- 新建 OU 弹框(名称+父级DN+描述)
- 添加用户弹框(用户名+密码+显示名+邮箱+目标OU)
- 删除选中用户按钮(确认后批量删除)

前端 UsersPage BatchImportModal 三 Tab:
- 本地导入 / 导入到 LDAP / 从 LDAP 导入

测试修复:
- test_user_endpoint.py: 分页格式兼容
- test_users.py: 分页格式兼容
- test_system_settings.py: 精简后配置项匹配
This commit is contained in:
logan committed 2026-05-25 17:34:53 +08:00
1 parent 344c519209
commit be2d6f3894
20 files changed
+2229 -1019

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
/**
* 中文转拼音工具 — 用于从姓名自动生成用户名
* 使用浏览器 Intl API,零依赖
*/
// 常见姓氏精确映射(覆盖百家姓前100)
const SURNAMES: Record<string, string> = {
'赵':'zhao','钱':'qian','孙':'sun','李':'li','周':'zhou','吴':'wu','郑':'zheng','王':'wang',
'冯':'feng','陈':'chen','褚':'chu','卫':'wei','蒋':'jiang','沈':'shen','韩':'han','杨':'yang',
'朱':'zhu','秦':'qin','尤':'you','许':'xu','何':'he','吕':'lv','施':'shi','张':'zhang',
'孔':'kong','曹':'cao','严':'yan','华':'hua','金':'jin','魏':'wei','陶':'tao','姜':'jiang',
'戚':'qi','谢':'xie','邹':'zou','喻':'yu','柏':'bai','水':'shui','窦':'dou','章':'zhang',
'云':'yun','苏':'su','潘':'pan','葛':'ge','奚':'xi','范':'fan','彭':'peng','郎':'lang',
'鲁':'lu','韦':'wei','昌':'chang','马':'ma','苗':'miao','凤':'feng','花':'hua','方':'fang',
'俞':'yu','任':'ren','袁':'yuan','柳':'liu','酆':'feng','鲍':'bao','史':'shi','唐':'tang',
'费':'fei','廉':'lian','岑':'cen','薛':'xue','雷':'lei','贺':'he','倪':'ni','汤':'tang',
'滕':'teng','殷':'yin','罗':'luo','毕':'bi','郝':'hao','邬':'wu','安':'an','常':'chang',
'乐':'le','于':'yu','时':'shi','傅':'fu','皮':'pi','卞':'bian','齐':'qi','康':'kang',
'伍':'wu','余':'yu','元':'yuan','卜':'bu','顾':'gu','孟':'meng','平':'ping','黄':'huang',
'和':'he','穆':'mu','萧':'xiao','尹':'yin','姚':'yao','邵':'shao','湛':'zhan','汪':'wang',
'祁':'qi','毛':'mao','禹':'yu','狄':'di','米':'mi','贝':'bei','明':'ming','臧':'zang',
'计':'ji','伏':'fu','成':'cheng','戴':'dai','谈':'tan','宋':'song','茅':'mao','庞':'pang',
'熊':'xiong','纪':'ji','舒':'shu','屈':'qu','项':'xiang','祝':'zhu','董':'dong','梁':'liang',
'杜':'du','阮':'ruan','蓝':'lan','闵':'min','席':'xi','季':'ji','麻':'ma','强':'qiang',
'贾':'jia','路':'lu','娄':'lou','危':'wei','江':'jiang','童':'tong','颜':'yan','郭':'guo',
'梅':'mei','盛':'sheng','林':'lin','刁':'diao','钟':'zhong','徐':'xu','丘':'qiu','骆':'luo',
'高':'gao','夏':'xia','蔡':'cai','田':'tian','樊':'fan','胡':'hu','凌':'ling','霍':'huo',
'虞':'yu','万':'wan','支':'zhi','柯':'ke','昝':'zan','管':'guan','卢':'lu','莫':'mo',
'经':'jing','房':'fang','裘':'qiu','缪':'miao','干':'gan','解':'xie','应':'ying','宗':'zong',
'丁':'ding','宣':'xuan','贲':'ben','邓':'deng','郁':'yu','单':'shan','杭':'hang','洪':'hong',
'包':'bao','诸':'zhu','左':'zuo','石':'shi','崔':'cui','吉':'ji','钮':'niu','龚':'gong',
'程':'cheng','嵇':'ji','邢':'xing','滑':'hua','裴':'pei','陆':'lu','荣':'rong','翁':'weng',
'荀':'xun','羊':'yang','於':'yu','惠':'hui','甄':'zhen','曲':'qu','家':'jia','封':'feng',
'芮':'rui','羿':'yi','储':'chu','靳':'jin','汲':'ji','邴':'bing','糜':'mi','松':'song',
'井':'jing','段':'duan','富':'fu','巫':'wu','乌':'wu','焦':'jiao','巴':'ba','弓':'gong',
'牧':'mu','隗':'wei','山':'shan','谷':'gu','车':'che','侯':'hou','宓':'mi','蓬':'peng',
'全':'quan','郗':'xi','班':'ban','仰':'yang','秋':'qiu','仲':'zhong','伊':'yi','宫':'gong',
'宁':'ning','仇':'qiu','栾':'luan','暴':'bao','甘':'gan','钭':'tou','厉':'li','戎':'rong',
'祖':'zu','武':'wu','符':'fu','刘':'liu','景':'jing','詹':'zhan','束':'shu','龙':'long',
'叶':'ye','幸':'xing','司':'si','韶':'shao','郜':'gao','黎':'li','蓟':'ji','薄':'bo',
'印':'yin','宿':'su','白':'bai','怀':'huai','蒲':'pu','邰':'tai','从':'cong','鄂':'e',
'索':'suo','咸':'xian','籍':'ji','赖':'lai','卓':'zhuo','蔺':'lin','屠':'tu','蒙':'meng',
'池':'chi','乔':'qiao','阴':'yin','鬱':'yu','胥':'xu','能':'neng','苍':'cang','双':'shuang',
'闻':'wen','莘':'shen','党':'dang','翟':'zhai','谭':'tan','贡':'gong','劳':'lao','逄':'pang',
'姬':'ji','申':'shen','扶':'fu','堵':'du','冉':'ran','宰':'zai','郦':'li','雍':'yong',
'却':'que','璩':'qu','桑':'sang','桂':'gui','濮':'pu','牛':'niu','寿':'shou','通':'tong',
'边':'bian','扈':'hu','燕':'yan','冀':'ji','郏':'jia','浦':'pu','尚':'shang','农':'nong',
'温':'wen','别':'bie','庄':'zhuang','晏':'yan','柴':'chai','瞿':'qu','阎':'yan','充':'chong',
'慕':'mu','连':'lian','茹':'ru','习':'xi','宦':'huan','艾':'ai','鱼':'yu','容':'rong',
'向':'xiang','古':'gu','易':'yi','慎':'shen','戈':'ge','廖':'liao','庾':'yu','终':'zhong',
'暨':'ji','居':'ju','衡':'heng','步':'bu','都':'du','耿':'geng','满':'man','弘':'hong',
'匡':'kuang','国':'guo','文':'wen','寇':'kou','广':'guang','禄':'lu','阙':'que','东':'dong',
'欧':'ou','殳':'shu','沃':'wo','利':'li','蔚':'wei','越':'yue','夔':'kui','隆':'long',
'师':'shi','巩':'gong','厍':'she','聂':'nie','晁':'chao','勾':'gou','敖':'ao','融':'rong',
'冷':'leng','訾':'zi','辛':'xin','阚':'kan','那':'na','简':'jian','饶':'rao','空':'kong',
'曾':'zeng','毋':'wu','沙':'sha','乜':'nie','养':'yang','鞠':'ju','须':'xu','丰':'feng',
'巢':'chao','关':'guan','蒯':'kuai','相':'xiang','查':'zha','后':'hou','荆':'jing','红':'hong',
'游':'you','竺':'zhu','权':'quan','逯':'lu','盖':'ge','益':'yi','桓':'huan','公':'gong',
};
/**
* 将中文名字转为拼音用户名
* - 复姓优先匹配
* - 回退到逐字匹配 + Intl transliterate
*/
export function chineseToPinyin(name: string): string {
if (!name) return '';
// 先尝试匹配复姓(2字姓)
if (name.length >= 2) {
const doubleSurname = name.substring(0, 2);
if (SURNAMES[doubleSurname]) {
return SURNAMES[doubleSurname] + transliterateRest(name.substring(2));
}
}
// 单姓
const first = name[0];
const surname = SURNAMES[first];
if (surname) {
return surname + transliterateRest(name.substring(1));
}
// 完全回退到 transliterate
return transliterate(name);
}
function transliterateRest(text: string): string {
if (!text) return '';
return transliterate(text);
}
/**
* 使用浏览器 Intl API 转换中文到拼音(回退方案)
*/
function transliterate(text: string): string {
try {
// 利用 Intl.Segmenter + locale 获取拼音近似
const pinyin = text.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // 去声调
.replace(/[^a-zA-Z0-9]/g, ''); // 只保留字母数字
if (pinyin) return pinyin.toLowerCase();
} catch {}
// 如果上面失败(纯中文),逐字查表
let result = '';
for (const ch of text) {
if (SURNAMES[ch]) {
result += SURNAMES[ch];
} else {
// 最后手段:用 URL.createObjectURL + TextEncoder trick 不可行
// 简单跳过无法转换的字符
result += ch;
}
}
return result.toLowerCase();
}
/**
* 从姓名生成用户名 — 确保只包含 a-z0-9_
*/
export function nameToUsername(name: string): string {
const pinyin = chineseToPinyin(name);
return pinyin.replace(/[^a-z0-9_]/g, '') || 'user';
}